Split Sieve/Filters code from app.js so that i can work on the new Sieve GUI

This commit is contained in:
djmaze 2022-03-11 10:26:25 +01:00
parent ec6a79d9d6
commit d6dc4d291c
23 changed files with 460 additions and 490 deletions

View file

@ -139,23 +139,25 @@ RainLoop 1.15 vs SnappyMail
|js/* |RainLoop |Snappy |
|--------------- |--------: |--------: |
|admin.js |2.158.025 | 79.040 |
|app.js |4.215.733 | 431.641 |
|admin.js |2.158.025 | 79.018 |
|app.js |4.215.733 | 407.680 |
|boot.js | 672.433 | 1.996 |
|libs.js | 647.679 | 200.131 |
|sieve.js | 0 | 30.809 |
|polyfills.js | 325.908 | 0 |
|serviceworker.js | 0 | 285 |
|TOTAL |8.019.778 | 713.093 |
|TOTAL |8.019.778 | 719.919 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|admin.min.js | 255.514 | 39.272 | 73.899 | 13.076 | 60.674 | 11.727 |
|app.min.js | 516.000 | 207.038 |140.430 | 66.265 |110.657 | 56.788 |
|admin.min.js | 255.514 | 39.256 | 73.899 | 13.076 | 60.674 | 11.702 |
|app.min.js | 516.000 | 194.277 |140.430 | 62.348 |110.657 | 53.485 |
|boot.min.js | 66.456 | 1.230 | 22.553 | 768 | 20.043 | 619 |
|libs.min.js | 574.626 | 96.201 |177.280 | 35.522 |151.855 | 31.746 |
|sieve.min.js | 0 | 15.009 | 0 | 5.228 | 0 | 4.702 |
|polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 |
|TOTAL |1.445.204 | 343.741 |425.477 |115.631 |353.301 |100.880 |
|TOTAL (no admin) |1.189.690 | 304.469 |351.061 |102.555 |292.627 | 89.153 |
|TOTAL |1.445.204 | 345.973 |425.477 |116.942 |353.301 |102.254 |
|TOTAL (no admin) |1.189.690 | 306.717 |351.061 |103.866 |292.627 | 90.552 |
For a user its around 69% smaller and faster than traditional RainLoop.

View file

@ -67,8 +67,8 @@ fetchFolderInformation = (fCallback, folder, list = []) => {
/**
* @param {Array=} aDisabled
* @param {Array=} aHeaderLines
* @param {Function=} fDisableCallback
* @param {Function=} fRenameCallback
* @param {Function=} fDisableCallback
* @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView
* @returns {Array}
*/

View file

@ -29,7 +29,7 @@ class AbstractView {
/*
onBuild() {}
onBeforeShow() {} // Happens before: hidden = false
beforeShow() {} // Happens before: hidden = false
onShow() {} // Happens after: hidden = false
onHide() {}
*/
@ -73,7 +73,7 @@ export class AbstractViewPopup extends AbstractView
onClose() {}
/*
onBeforeShow() {} // Happens before showModal()
beforeShow() {} // Happens before showModal()
onShow() {} // Happens after showModal()
afterShow() {} // Happens after showModal() animation transitionend
onHide() {} // Happens before animation transitionend
@ -112,7 +112,7 @@ export class AbstractViewSettings
{
/*
onBuild(viewModelDom) {}
onBeforeShow() {}
beforeShow() {}
onShow() {}
onHide() {}
viewModelDom

View file

@ -214,7 +214,7 @@ const
vmScreen.onShow && vmScreen.onShow();
forEachViewModel(vmScreen, (vm, dom) => {
vm.onBeforeShow && vm.onBeforeShow();
vm.beforeShow && vm.beforeShow();
dom.hidden = false;
vm.onShow && vm.onShow();
autofocus(dom);
@ -243,7 +243,7 @@ export const
if (vm) {
params = params || [];
vm.onBeforeShow && vm.onBeforeShow(...params);
vm.beforeShow && vm.beforeShow(...params);
vm.modalVisible(true);

View file

@ -69,7 +69,7 @@ export class AbstractSettingsScreen extends AbstractScreen {
this.oCurrentSubScreen = settingsScreen;
// show
settingsScreen.onBeforeShow && settingsScreen.onBeforeShow();
settingsScreen.beforeShow && settingsScreen.beforeShow();
settingsScreen.viewModelDom.hidden = false;
settingsScreen.onShow && settingsScreen.onShow();

View file

@ -21,8 +21,6 @@ import { SystemDropDownUserView } from 'View/User/SystemDropDown';
import { SettingsMenuUserView } from 'View/User/Settings/Menu';
import { SettingsPaneUserView } from 'View/User/Settings/Pane';
//import { staticLink } from 'Common/Links';
export class SettingsUserScreen extends AbstractSettingsScreen {
constructor() {
super([SystemDropDownUserView, SettingsMenuUserView, SettingsPaneUserView]);
@ -41,7 +39,6 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
if (SettingsCapa('Sieve')) {
views.push(UserSettingsFilters);
// rl.loadScript(staticLink('js/sieve.js')).then(() => 0).catch(e => console.error(e));
}
if (SettingsCapa('AutoLogout') || SettingsCapa('OpenPGP') || SettingsCapa('GnuPG')) {

View file

@ -9,7 +9,7 @@ export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
this.config = ko.observableArray();
}
onBeforeShow() {
beforeShow() {
Remote.request('AdminSettingsGet', (iError, data) => {
if (!iError) {
const cfg = [],

View file

@ -1,97 +1,44 @@
import { getNotification } from 'Common/Translator';
import { forEachObjectValue } from 'Common/Utils';
import { addObservablesTo } from 'External/ko';
import { staticLink } from 'Common/Links';
import { FolderUserStore } from 'Stores/User/Folder';
import { SieveUserStore } from 'Stores/User/Sieve';
import Remote from 'Remote/User/Fetch';
import { SieveScriptModel } from 'Model/SieveScript';
import { showScreenPopup } from 'Knoin/Knoin';
import { SieveScriptPopupView } from 'View/Popup/SieveScript';
//export class UserSettingsFilters /*extends AbstractViewSettings*/ {
export class UserSettingsFilters /*extends AbstractViewSettings*/ {
constructor() {
this.scripts = SieveUserStore.scripts;
this.loading = ko.observable(false).extend({ debounce: 200 });
this.scripts = ko.observableArray();
this.loading = ko.observable(true).extend({ debounce: 200 });
addObservablesTo(this, {
serverError: false,
serverErrorDesc: ''
});
rl.loadScript(staticLink('js/sieve.js')).then(() => {
const Sieve = window.Sieve;
Sieve.folderList = FolderUserStore.folderList;
Sieve.serverError.subscribe(value => this.serverError(value));
Sieve.serverErrorDesc.subscribe(value => this.serverErrorDesc(value));
Sieve.loading.subscribe(value => this.loading(value));
Sieve.scripts.subscribe(value => this.scripts(value));
Sieve.updateList();
}).catch(e => console.error(e));
this.scriptForDeletion = ko.observable(null).askDeleteHelper();
}
setError(text) {
this.serverError(true);
this.serverErrorDesc(text);
}
updateList() {
if (!this.loading()) {
this.loading(true);
this.serverError(false);
Remote.request('Filters', (iError, data) => {
this.loading(false);
this.scripts([]);
if (iError) {
SieveUserStore.capa([]);
this.setError(getNotification(iError));
} else {
SieveUserStore.capa(data.Result.Capa);
/*
this.scripts(
data.Result.Scripts.map(aItem => SieveScriptModel.reviveFromJson(aItem)).filter(v => v)
);
*/
forEachObjectValue(data.Result.Scripts, value => {
value = SieveScriptModel.reviveFromJson(value);
value && this.scripts.push(value)
});
}
});
}
}
addScript() {
showScreenPopup(SieveScriptPopupView, [new SieveScriptModel()]);
this.editScript();
}
editScript(script) {
showScreenPopup(SieveScriptPopupView, [script]);
window.Sieve.ScriptView.showModal(script ? [script] : null);
}
deleteScript(script) {
this.serverError(false);
Remote.request('FiltersScriptDelete',
(iError, data) => {
if (iError) {
this.setError((data && data.ErrorMessageAdditional) || getNotification(iError));
} else {
this.scripts.remove(script);
}
},
{name:script.name()}
);
window.Sieve.deleteScript(script);
}
toggleScript(script) {
let name = script.active() ? '' : script.name();
this.serverError(false);
Remote.request('FiltersScriptActivate',
(iError, data) => {
if (iError) {
this.setError((data && data.ErrorMessageAdditional) || iError)
} else {
this.scripts.forEach(script => script.active(script.name() === name));
}
},
{name:name}
);
window.Sieve.toggleScript(script);
}
onBuild(oDom) {
@ -103,6 +50,6 @@ export class UserSettingsFilters /*extends AbstractViewSettings*/ {
}
onShow() {
this.updateList();
window.Sieve && window.Sieve.updateList();
}
}

View file

@ -1,73 +0,0 @@
import { getNotification } from 'Common/Translator';
import Remote from 'Remote/User/Fetch';
//export class UserSettingsFilters /*extends AbstractViewSettings*/ {
export class UserSettingsSieve /*extends AbstractViewSettings*/ {
constructor() {
const Sieve = window.Sieve;
this.scripts = Sieve.scripts;
this.loading = Sieve.loading;
this.serverError = Sieve.serverError;
this.serverErrorDesc = Sieve.serverErrorDesc;
this.scriptForDeletion = ko.observable(null).askDeleteHelper();
}
setError(text) {
this.serverError(true);
this.serverErrorDesc(text);
}
updateList() {
window.Sieve.updateList();
}
addScript() {
window.Sieve.ScriptView.showModal();
}
editScript(script) {
window.Sieve.ScriptView.showModal([script]);
}
deleteScript(script) {
this.serverError(false);
Remote.request('FiltersScriptDelete',
(iError, data) => {
if (iError) {
this.setError((data && data.ErrorMessageAdditional) || getNotification(iError));
} else {
this.scripts.remove(script);
}
},
{name:script.name()}
);
}
toggleScript(script) {
let name = script.active() ? '' : script.name();
this.serverError(false);
Remote.request('FiltersScriptActivate',
(iError, data) => {
if (iError) {
this.setError((data && data.ErrorMessageAdditional) || iError)
} else {
this.scripts.forEach(script => script.active(script.name() === name));
}
},
{name:name}
);
}
onBuild(oDom) {
oDom.addEventListener('click', event => {
const el = event.target.closestWithin('.script-item .e-action', oDom),
script = el && ko.dataFor(el);
script && this.editScript(script);
});
}
onShow() {
this.updateList();
}
}

121
dev/Sieve/Model/Abstract.js Normal file
View file

@ -0,0 +1,121 @@
import { forEachObjectValue, forEachObjectEntry, koComputable } from 'Sieve/Utils';
function typeCast(curValue, newValue) {
if (null != curValue) {
switch (typeof curValue)
{
case 'boolean': return 0 != newValue && !!newValue;
case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
case 'string': return null != newValue ? '' + newValue : '';
case 'object':
if (curValue.constructor.reviveFromJson) {
return curValue.constructor.reviveFromJson(newValue);
}
if (Array.isArray(curValue) && !Array.isArray(newValue))
return [];
}
}
return newValue;
}
export class AbstractModel {
constructor() {
/*
if (new.target === AbstractModel) {
throw new Error("Can't instantiate AbstractModel!");
}
*/
this.disposables = [];
}
addObservables(observables) {
forEachObjectEntry(observables, (key, value) =>
this[key] || (this[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value))
);
}
addComputables(computables) {
forEachObjectEntry(computables, (key, fn) => this[key] = koComputable(fn));
}
addSubscribables(subscribables) {
forEachObjectEntry(subscribables, (key, fn) => this.disposables.push( this[key].subscribe(fn) ) );
}
/** Called by delegateRunOnDestroy */
onDestroy() {
/** dispose ko subscribables */
this.disposables.forEach(disposable => {
disposable && typeof disposable.dispose === 'function' && disposable.dispose();
});
/** clear object entries */
// forEachObjectEntry(this, (key, value) => {
forEachObjectValue(this, value => {
/** clear CollectionModel */
let arr = ko.isObservableArray(value) ? value() : value;
arr && arr.onDestroy && arr.onDestroy();
/** destroy ko.observable/ko.computed? */
// dispose(value);
/** clear object value */
// this[key] = null; // TODO: issue with Contacts view
});
// this.disposables = [];
}
/**
* @static
* @param {FetchJson} json
* @returns {boolean}
*/
static validJson(json) {
return !!(json && ('Object/'+this.name.replace('Model', '') === json['@Object']));
}
/**
* @static
* @param {FetchJson} json
* @returns {*Model}
*/
static reviveFromJson(json) {
let obj = this.validJson(json) ? new this() : null;
obj && obj.revivePropertiesFromJson(json);
return obj;
}
revivePropertiesFromJson(json) {
let model = this.constructor;
if (!model.validJson(json)) {
return false;
}
forEachObjectEntry(json, (key, value) => {
if ('@' !== key[0]) try {
key = key[0].toLowerCase() + key.slice(1);
switch (typeof this[key])
{
case 'function':
if (ko.isObservable(this[key])) {
this[key](typeCast(this[key](), value));
// console.log('Observable ' + (typeof this[key]()) + ' ' + (model.name) + '.' + key + ' revived');
}
// else console.log(model.name + '.' + key + ' is a function');
break;
case 'boolean':
case 'number':
case 'object':
case 'string':
this[key] = typeCast(this[key], value);
break;
// fall through
case 'undefined':
default:
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
}
} catch (e) {
console.log(model.name + '.' + key);
console.error(e);
}
});
return true;
}
}

View file

@ -1,13 +1,11 @@
import { koArrayWithDestroy } from 'External/ko';
import { koArrayWithDestroy } from 'Sieve/Utils';
import { arrayLength, pString } from 'Common/Utils';
import { i18n } from 'Common/Translator';
import { getFolderFromCacheList } from 'Common/Cache';
//import { getFolderFromCacheList } from 'Common/Cache';
import { AccountUserStore } from 'Stores/User/Account';
//import { AccountUserStore } from 'Stores/User/Account';
import { FilterConditionModel } from 'Model/FilterCondition';
import { AbstractModel } from 'Knoin/AbstractModel';
import { FilterConditionModel } from 'Sieve/Model/FilterCondition';
import { AbstractModel } from 'Sieve/Model/Abstract';
/**
* @enum {string}
@ -66,9 +64,10 @@ export class FilterModel extends AbstractModel {
this.conditions = koArrayWithDestroy();
const fGetRealFolderName = (folderFullName) => {
const folder = getFolderFromCacheList(folderFullName);
return folder ? folder.fullName.replace('.' === folder.delimiter ? /\./ : /[\\/]+/, ' / ') : folderFullName;
const fGetRealFolderName = folderFullName => {
// const folder = getFolderFromCacheList(folderFullName);
// return folder ? folder.fullName.replace('.' === folder.delimiter ? /\./ : /[\\/]+/, ' / ') : folderFullName;
return folderFullName;
};
this.addComputables({
@ -78,23 +77,23 @@ export class FilterModel extends AbstractModel {
switch (this.actionType()) {
case FilterAction.MoveTo:
result = i18n(root + 'MOVE_TO', {
result = rl.i18n(root + 'MOVE_TO', {
FOLDER: fGetRealFolderName(actionValue)
});
break;
case FilterAction.Forward:
result = i18n(root + 'FORWARD_TO', {
result = rl.i18n(root + 'FORWARD_TO', {
EMAIL: actionValue
});
break;
case FilterAction.Vacation:
result = i18n(root + 'VACATION_MESSAGE');
result = rl.i18n(root + 'VACATION_MESSAGE');
break;
case FilterAction.Reject:
result = i18n(root + 'REJECT');
result = rl.i18n(root + 'REJECT');
break;
case FilterAction.Discard:
result = i18n(root + 'DISCARD');
result = rl.i18n(root + 'DISCARD');
break;
// no default
}
@ -213,7 +212,7 @@ export class FilterModel extends AbstractModel {
}
setRecipients() {
this.actionValueFourth(AccountUserStore.getEmailAddresses().join(', '));
// this.actionValueFourth(AccountUserStore.getEmailAddresses().join(', '));
}
/**
@ -224,16 +223,10 @@ export class FilterModel extends AbstractModel {
static reviveFromJson(json) {
const filter = super.reviveFromJson(json);
if (filter) {
filter.id = pString(json.ID);
filter.conditions([]);
if (arrayLength(json.Conditions)) {
filter.conditions(
json.Conditions.map(aData => FilterConditionModel.reviveFromJson(aData)).filter(v => v)
);
}
filter.id = filter.id ? '' + filter.id : '';
filter.conditions(
json.Conditions ? json.Conditions.map(aData => FilterConditionModel.reviveFromJson(aData)).filter(v => v) : []
);
filter.actionKeep(0 != json.Keep);
filter.actionNoStop(0 == json.Stop);
filter.actionMarkAsRead(1 == json.MarkAsRead);

View file

@ -1,6 +1,6 @@
import { koComputable } from 'External/ko';
import { koComputable } from 'Sieve/Utils';
import { AbstractModel } from 'Knoin/AbstractModel';
import { AbstractModel } from 'Sieve/Model/Abstract';
/**
* @enum {string}

View file

@ -1,7 +1,6 @@
import { AbstractModel } from 'Knoin/AbstractModel';
import { FilterModel } from 'Model/Filter';
import { arrayLength, pString, b64EncodeJSON } from 'Common/Utils';
import { koArrayWithDestroy } from 'External/ko';
import { AbstractModel } from 'Sieve/Model/Abstract';
import { FilterModel } from 'Sieve/Model/Filter';
import { koArrayWithDestroy } from 'Sieve/Utils';
const SIEVE_FILE_NAME = 'rainloop.user';
@ -153,12 +152,12 @@ function filtersToSieveScript(filters)
subject = ':subject ' + quote(StripSpaces(paramValue)) + ' ';
}
paramValue = pString(filter.actionValueThird()).trim();
paramValue = (filter.actionValueThird() || '').trim();
if (paramValue.length) {
days = Math.max(1, parseInt(paramValue, 10));
}
paramValue = pString(filter.actionValueFourth()).trim()
paramValue = (filter.actionValueFourth() || '').trim()
if (paramValue.length) {
paramValue = paramValue.split(',').map(email =>
email.trim().length ? quote(email) : ''
@ -214,7 +213,7 @@ function filtersToSieveScript(filters)
'/*',
'BEGIN:FILTER:' + filter.id,
'BEGIN:HEADER',
b64EncodeJSON(filter.toJson()).match(split).join(eol) + 'END:HEADER',
btoa(unescape(encodeURIComponent(JSON.stringify(filter.toJson())))).match(split).join(eol) + 'END:HEADER',
'*/',
filter.enabled() ? '' : '/* @Filter is disabled ',
filterToString(filter, require),
@ -317,7 +316,7 @@ export class SieveScriptModel extends AbstractModel
if (script) {
if (script.allowFilters()) {
script.filters(
arrayLength(json.filters)
Array.isArray(json.filters) && json.filters.length
? json.filters.map(aData => FilterModel.reviveFromJson(aData)).filter(v => v)
: sieveScriptToFilters(script.body())
);

View file

@ -1,4 +1,89 @@
import { SieveScriptModel } from 'Sieve/Model/Script';
export const
// import { i18n } from 'Common/Translator';
i18n = rl.i18n,
// import { forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),
// import { koArrayWithDestroy } from 'External/ko';
// With this we don't need delegateRunOnDestroy
koArrayWithDestroy = data => {
data = ko.observableArray(data);
data.subscribe(changes =>
changes.forEach(item =>
'deleted' === item.status && null == item.moved && item.value.onDestroy && item.value.onDestroy()
)
, data, 'arrayChange');
return data;
},
// import { koComputable } from 'External/ko';
koComputable = fn => ko.computed(fn, {'pure':true}),
arrayToString = (arr, separator) =>
arr.map(item => item.toString ? item.toString() : item).join(separator);
arr.map(item => item.toString ? item.toString() : item).join(separator),
/*
getNotificationMessage = code => {
let key = getKeyByValue(Notification, code);
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
rl.i18n('NOTIFICATIONS/')
},
getNotification = (code, message = '', defCode = 0) => {
code = parseInt(code, 10) || 0;
if (Notification.ClientViewError === code && message) {
return message;
}
return getNotificationMessage(code)
|| getNotificationMessage(parseInt(defCode, 10))
|| '';
},
*/
getNotification = code => 'ERROR ' + code,
Remote = rl.app.Remote,
// capabilities
capa = ko.observableArray(),
// Sieve scripts SieveScriptModel
scripts = koArrayWithDestroy(),
loading = ko.observable(false),
serverError = ko.observable(false),
serverErrorDesc = ko.observable(''),
setError = text => {
serverError(true);
serverErrorDesc(text);
},
updateList = () => {
if (!loading()) {
loading(true);
serverError(false);
Remote.request('Filters', (iError, data) => {
loading(false);
scripts([]);
if (iError) {
capa([]);
setError(getNotification(iError));
} else {
capa(data.Result.Capa);
/*
scripts(
data.Result.Scripts.map(aItem => SieveScriptModel.reviveFromJson(aItem)).filter(v => v)
);
*/
forEachObjectValue(data.Result.Scripts, value => {
value = SieveScriptModel.reviveFromJson(value);
value && scripts.push(value)
});
}
});
}
};

View file

@ -1,19 +1,77 @@
import ko from 'ko';
import { koComputable } from 'External/ko';
import { FilterAction } from 'Sieve/Model/Filter';
import { FilterConditionField, FilterConditionType } from 'Sieve/Model/FilterCondition';
/*
import { SettingsUserStore } from 'Stores/User/Settings';
*/
import { FilterAction } from 'Model/Filter';
import { FilterConditionField, FilterConditionType } from 'Model/FilterCondition';
import { SettingsGet } from 'Common/Globals';
import { defaultOptionsAfterRender } from 'Common/Utils';
import { i18n, initOnStartOrLangChange } from 'Common/Translator';
import {
capa,
i18n,
koComputable
} from 'Sieve/Utils';
import { SieveUserStore } from 'Stores/User/Sieve';
const
// import { defaultOptionsAfterRender } from 'Common/Utils';
defaultOptionsAfterRender = (domItem, item) =>
domItem && item && undefined !== item.disabled
&& domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
import { AbstractViewPopup } from 'Knoin/AbstractViews';
// import { folderListOptionsBuilder } from 'Common/Folders';
/**
* @param {Array=} aDisabled
* @param {Array=} aHeaderLines
* @param {Function=} fRenameCallback
* @returns {Array}
*/
folderListOptionsBuilder = (
aDisabled,
aHeaderLines,
fRenameCallback
) => {
const
aResult = [],
sDeepPrefix = '\u00A0\u00A0\u00A0',
showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
import { folderListOptionsBuilder } from 'Common/Folders';
foldersWalk = folders => {
folders.forEach(oItem => {
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
aResult.push({
id: oItem.fullName,
name:
sDeepPrefix.repeat(oItem.deep) +
fRenameCallback(oItem),
system: false,
disabled: !oItem.selectable() || aDisabled.includes(oItem.fullName)
});
}
export class FilterPopupView extends AbstractViewPopup {
if (oItem.subFolders.length) {
foldersWalk(oItem.subFolders());
}
});
};
fRenameCallback = fRenameCallback || (oItem => oItem.name());
Array.isArray(aDisabled) || (aDisabled = []);
Array.isArray(aHeaderLines) && aHeaderLines.forEach(line =>
aResult.push({
id: line[0],
name: line[1],
system: false,
disabled: false
})
);
// FolderUserStore.folderList()
foldersWalk(window.Sieve.folderList() || []);
return aResult;
};
export class FilterPopupView extends rl.pluginPopupView {
constructor() {
super('Filter');
@ -27,7 +85,7 @@ export class FilterPopupView extends AbstractViewPopup {
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
this.folderSelectList = koComputable(() =>
folderListOptionsBuilder(
[SettingsGet('SieveAllowFileintoInbox') ? '' : 'INBOX'],
[rl.settings.get('SieveAllowFileintoInbox') ? '' : 'INBOX'],
[['', '']],
item => item ? item.localName() : ''
)
@ -39,9 +97,7 @@ export class FilterPopupView extends AbstractViewPopup {
key => this[key] = ko.observableArray()
);
initOnStartOrLangChange(this.populateOptions.bind(this));
SieveUserStore.capa.subscribe(this.populateOptions, this);
this.populateOptions();
}
saveFilter() {
@ -77,11 +133,10 @@ export class FilterPopupView extends AbstractViewPopup {
// this.actionTypeOptions.push({id: FilterAction.None,
// name: i18n('GLOBAL/NONE')});
const modules = SieveUserStore.capa;
if (modules) {
this.allowMarkAsRead(modules.includes('imap4flags'));
if (capa) {
this.allowMarkAsRead(capa.includes('imap4flags'));
if (modules.includes('fileinto')) {
if (capa.includes('fileinto')) {
this.actionTypeOptions.push({
id: FilterAction.MoveTo,
name: i18nFilter('ACTION_MOVE_TO')
@ -92,22 +147,22 @@ export class FilterPopupView extends AbstractViewPopup {
});
}
if (modules.includes('reject')) {
if (capa.includes('reject')) {
this.actionTypeOptions.push({ id: FilterAction.Reject, name: i18nFilter('ACTION_REJECT') });
}
if (modules.includes('vacation')) {
if (capa.includes('vacation')) {
this.actionTypeOptions.push({
id: FilterAction.Vacation,
name: i18nFilter('ACTION_VACATION_MESSAGE')
});
}
if (modules.includes('body')) {
if (capa.includes('body')) {
this.fieldOptions.push({ id: FilterConditionField.Body, name: i18nFilter('FIELD_BODY') });
}
if (modules.includes('regex')) {
if (capa.includes('regex')) {
this.typeOptions.push({ id: FilterConditionType.Regex, name: 'Regex' });
}
}
@ -129,7 +184,8 @@ export class FilterPopupView extends AbstractViewPopup {
this.filter().removeCondition(oConditionToDelete);
}
onShow(oFilter, fTrueCallback, bEdit) {
beforeShow(oFilter, fTrueCallback, bEdit) {
// onShow(oFilter, fTrueCallback, bEdit) {
this.isNew(!bEdit);
this.fTrueCallback = fTrueCallback;
@ -138,6 +194,8 @@ export class FilterPopupView extends AbstractViewPopup {
this.selectedFolderValue(oFilter.actionValue());
bEdit || oFilter.nameFocused(true);
this.populateOptions();
}
afterShow() {

View file

@ -1,24 +1,22 @@
import ko from 'ko';
import { FilterModel } from 'Sieve/Model/Filter';
import { SieveScriptModel } from 'Sieve/Model/Script';
import { getNotification, i18nToNodes } from 'Common/Translator';
import { addObservablesTo } from 'External/ko';
import Remote from 'Remote/User/Fetch';
import { FilterModel } from 'Model/Filter';
import { SieveUserStore } from 'Stores/User/Sieve';
import { showScreenPopup } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
import { FilterPopupView } from 'View/Popup/Filter';
import { FilterPopupView } from 'Sieve/View/Filter';
//import { parseScript } from 'Sieve/Parser';
export class SieveScriptPopupView extends AbstractViewPopup {
import {
capa,
scripts,
getNotification,
Remote
} from 'Sieve/Utils';
export class SieveScriptPopupView extends rl.pluginPopupView {
constructor() {
super('SieveScript');
addObservablesTo(this, {
this.addObservables({
saveError: false,
saveErrorText: '',
rawActive: false,
@ -26,7 +24,7 @@ export class SieveScriptPopupView extends AbstractViewPopup {
script: null
});
this.sieveCapabilities = SieveUserStore.capa.join(' ');
this.sieveCapabilities = capa.join(' ');
this.saving = false;
this.filterForDeletion = ko.observable(null).askDeleteHelper();
@ -40,7 +38,7 @@ export class SieveScriptPopupView extends AbstractViewPopup {
return;
}
if (!script.exists() && SieveUserStore.scripts.find(item => item.name() === script.name())) {
if (!script.exists() && scripts.find(item => item.name() === script.name())) {
script.nameError(true);
return;
}
@ -60,7 +58,7 @@ export class SieveScriptPopupView extends AbstractViewPopup {
self.saveError(true);
self.saveErrorText((data && data.ErrorMessageAdditional) || getNotification(iError));
} else {
script.exists() || SieveUserStore.scripts.push(script);
script.exists() || scripts.push(script);
script.exists(true);
script.hasChanges(false);
}
@ -78,7 +76,7 @@ export class SieveScriptPopupView extends AbstractViewPopup {
/* this = SieveScriptModel */
const filter = new FilterModel();
filter.generateID();
showScreenPopup(FilterPopupView, [
FilterPopupView.showModal([
filter,
() => this.filters.push(filter)
]);
@ -86,13 +84,14 @@ export class SieveScriptPopupView extends AbstractViewPopup {
editFilter(filter) {
const clonedFilter = filter.cloneSelf();
showScreenPopup(FilterPopupView, [
FilterPopupView.showModal([
clonedFilter,
() => {
const script = this.script(),
filters = script.filters(),
index = filters.indexOf(filter);
if (-1 < index) {
// script.filters.splice(index, 1, clonedFilter);
filters[index] = clonedFilter;
script.filters(filters);
}
@ -118,7 +117,9 @@ export class SieveScriptPopupView extends AbstractViewPopup {
});
}
onShow(oScript) {
beforeShow(oScript) {
// onShow(oScript) {
oScript = oScript || new SieveScriptModel();
let raw = !oScript.allowFilters();
this.script(oScript);
this.rawActive(raw);
@ -132,9 +133,4 @@ export class SieveScriptPopupView extends AbstractViewPopup {
console.log(tree.join('\r\n'));
*/
}
afterShow() {
// Sometimes not everything is translated, try again
i18nToNodes(this.viewModelDom);
}
}

View file

@ -36,7 +36,7 @@ export class LanguagesPopupView extends AbstractViewPopup {
this.languages().forEach(item => item.selected(item.key === currentLang));
}
onBeforeShow() {
beforeShow() {
this.fLang = null;
this.userLanguage('');

View file

@ -1,206 +1,56 @@
import { parseScript } from 'Sieve/Parser';
import {
capa,
scripts,
loading,
serverError,
serverErrorDesc,
setError,
updateList,
getNotification,
Remote
} from 'Sieve/Utils';
import { FilterModel } from 'Model/Filter';
import { SieveScriptModel } from 'Model/SieveScript';
import { FilterPopupView } from 'View/Popup/Filter';
//import { getNotification, i18nToNodes } from 'Common/Translator';
import { forEachObjectValue } from 'Common/Utils';
import { koArrayWithDestroy } from 'External/ko';
import { SieveScriptPopupView } from 'Sieve/View/Script';
// SieveUserStore
const
/*
getNotificationMessage = code => {
let key = getKeyByValue(Notification, code);
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
rl.i18n('NOTIFICATIONS/')
},
getNotification = (code, message = '', defCode = 0) => {
code = parseInt(code, 10) || 0;
if (Notification.ClientViewError === code && message) {
return message;
}
window.Sieve = {
capa: capa,
scripts: scripts,
setError: setError,
updateList: updateList,
loading: loading,
serverError: serverError,
serverErrorDesc: serverErrorDesc,
ScriptView: SieveScriptPopupView,
return getNotificationMessage(code)
|| getNotificationMessage(parseInt(defCode, 10))
|| '';
},
*/
getNotification = code => 'ERROR ' + code,
folderList: null,
Remote = rl.app.Remote,
Sieve = {
// capabilities
capa: ko.observableArray(),
// Sieve scripts SieveScriptModel
scripts: koArrayWithDestroy(),
parseScript: parseScript,
setError: text => {
Sieve.serverError(true);
Sieve.serverErrorDesc(text);
},
updateList: () => {
if (!Sieve.loading()) {
Sieve.loading(true);
Sieve.serverError(false);
Remote.request('Filters', (iError, data) => {
Sieve.loading(false);
Sieve.scripts([]);
if (iError) {
Sieve.capa([]);
Sieve.setError(getNotification(iError));
} else {
Sieve.capa(data.Result.Capa);
/*
Sieve.scripts(
data.Result.Scripts.map(aItem => SieveScriptModel.reviveFromJson(aItem)).filter(v => v)
);
*/
forEachObjectValue(data.Result.Scripts, value => {
value = SieveScriptModel.reviveFromJson(value);
value && Sieve.scripts.push(value)
});
}
});
}
},
loading: ko.observable(false).extend({ debounce: 200 }),
serverError: ko.observable(false),
serverErrorDesc: ko.observable('')
};
Sieve.ScriptView = class SieveScriptPopupView extends rl.pluginPopupView {
constructor() {
super('SieveScript');
this.addObservables({
saveError: false,
saveErrorText: '',
rawActive: false,
allowToggle: false,
script: null
});
this.sieveCapabilities = Sieve.capa.join(' ');
this.saving = false;
this.filterForDeletion = ko.observable(null).askDeleteHelper();
}
saveScript() {
let self = this,
script = self.script();
if (!self.saving/* && script.hasChanges()*/) {
if (!script.verify()) {
return;
}
if (!script.exists() && Sieve.scripts.find(item => item.name() === script.name())) {
script.nameError(true);
return;
}
self.saving = true;
self.saveError(false);
if (self.allowToggle()) {
script.body(script.filtersToRaw());
}
Remote.request('FiltersScriptSave',
(iError, data) => {
self.saving = false;
if (iError) {
self.saveError(true);
self.saveErrorText((data && data.ErrorMessageAdditional) || getNotification(iError));
} else {
script.exists() || Sieve.scripts.push(script);
script.exists(true);
script.hasChanges(false);
}
},
script.toJson()
);
}
}
deleteFilter(filter) {
this.script().filters.remove(filter);
}
addFilter() {
/* this = SieveScriptModel */
const filter = new FilterModel();
filter.generateID();
FilterPopupView.showModal([
filter,
() => this.filters.push(filter)
]);
}
editFilter(filter) {
const clonedFilter = filter.cloneSelf();
FilterPopupView.showModal([
clonedFilter,
() => {
const script = this.script(),
filters = script.filters(),
index = filters.indexOf(filter);
if (-1 < index) {
filters[index] = clonedFilter;
script.filters(filters);
deleteScript: script => {
serverError(false);
Remote.request('FiltersScriptDelete',
(iError, data) => {
if (iError) {
setError((data && data.ErrorMessageAdditional) || getNotification(iError));
} else {
scripts.remove(script);
}
},
true
]);
{name:script.name()}
);
},
toggleScript(script) {
let name = script.active() ? '' : script.name();
serverError(false);
Remote.request('FiltersScriptActivate',
(iError, data) => {
if (iError) {
setError((data && data.ErrorMessageAdditional) || iError)
} else {
scripts.forEach(script => script.active(script.name() === name));
}
},
{name:name}
);
}
toggleFiltersRaw() {
let script = this.script(), notRaw = !this.rawActive();
if (notRaw) {
script.body(script.filtersToRaw());
script.hasChanges(script.hasChanges());
}
this.rawActive(notRaw);
}
onBuild(oDom) {
oDom.addEventListener('click', event => {
const el = event.target.closestWithin('td.e-action', oDom),
filter = el && ko.dataFor(el);
filter && this.editFilter(filter);
});
}
onShow(oScript) {
oScript = oScript || new SieveScriptModel();
let raw = !oScript.allowFilters();
this.script(oScript);
this.rawActive(raw);
this.allowToggle(!raw);
this.saveError(false);
/*
// TODO: Sieve GUI
let tree = parseScript(oScript.body(), oScript.name());
console.dir(tree);
console.log(tree.join('\r\n'));
*/
}
afterShow() {
// Sometimes not everything is translated, try again
// i18nToNodes(this.viewModelDom);
}
}
window.Sieve = Sieve;
};

View file

@ -21,7 +21,7 @@ class PluginPopupView extends rl.pluginPopupView
onBuild(dom) {}
// Happens before showModal()
onBeforeShow(...params) {}
beforeShow(...params) {}
// Happens after showModal()
onShow(...params) {}
// Happens after showModal() animation transitionend

View file

@ -5,44 +5,39 @@
<span data-i18n="POPUPS_FILTER/TITLE_EDIT_FILTER" data-bind="visible: !isNew()"></span>
</h3>
</header>
<div class="modal-body">
<div class="filter" data-bind="with: filter, i18nInit: filter">
<div data-bind="i18nInit: true">
<div class="modal-body" data-bind="with: filter, i18nInit: filter">
<div class="control-group" data-bind="css: {'error': nameError}">
<input type="text" class="span5"
data-bind="value: name, hasfocus: nameFocused"
autocorrect="off" autocapitalize="off" spellcheck="false"
data-i18n="[placeholder]GLOBAL/NAME">
</div>
<div class="control-group" data-bind="css: {'error': nameError}">
<input type="text" class="span5"
data-bind="value: name, hasfocus: nameFocused"
autocorrect="off" autocapitalize="off" spellcheck="false"
data-i18n="[placeholder]GLOBAL/NAME">
</div>
<div class="legend" data-i18n="POPUPS_FILTER/LEGEND_CONDITIONS"></div>
<div class="control-group" data-bind="visible: 1 < conditions().length">
<select class="span4" data-bind="value: conditionsType">
<option value="Any"
data-i18n="POPUPS_FILTER/SELECT_MATCH_ANY"></option>
<option value="All"
data-i18n="POPUPS_FILTER/SELECT_MATCH_ALL"></option>
</select>
</div>
<div data-bind="visible: conditions().length, foreach: conditions">
<div class="control-group" data-bind="css: {'error': valueError}" style="display:flex">
<div style="flex-grow:1" data-bind="css: {'error': valueError}, template: {'name': template(), 'data': $data}"></div>
<span class="delete-action button-delete fontastic" style="margin-top: 5px;"
data-bind="click: function (oCondition) { $root.removeCondition(oCondition); }">🗑</span>
</div>
</div>
<div class="control-group" data-bind="visible: 0 === conditions().length"
data-i18n="POPUPS_FILTER/ALL_INCOMING_MESSAGES_DESC"></div>
<div class="control-group">
<a class="btn" data-bind="click: addCondition, i18nInit: true" data-icon="✚" data-i18n="POPUPS_FILTER/BUTTON_ADD_CONDITION"></a>
</div>
<div class="legend" data-i18n="POPUPS_FILTER/LEGEND_ACTIONS"></div>
<select class="span3" data-bind="options: $root.actionTypeOptions, value: actionType, optionsText: 'name', optionsValue: 'id'"></select>
<div data-bind="template: {'name': actionTemplate()}, i18nUpdate: actionTemplate"></div>
<div class="legend" data-i18n="POPUPS_FILTER/LEGEND_CONDITIONS"></div>
<div class="control-group" data-bind="visible: 1 < conditions().length">
<select class="span4" data-bind="value: conditionsType">
<option value="Any"
data-i18n="POPUPS_FILTER/SELECT_MATCH_ANY"></option>
<option value="All"
data-i18n="POPUPS_FILTER/SELECT_MATCH_ALL"></option>
</select>
</div>
<div data-bind="visible: conditions().length, foreach: conditions">
<div class="control-group" data-bind="css: {'error': valueError}" style="display:flex">
<div style="flex-grow:1" data-bind="css: {'error': valueError}, template: {'name': template(), 'data': $data}"></div>
<span class="delete-action button-delete fontastic" style="margin-top: 5px;"
data-bind="click: function (oCondition) { $root.removeCondition(oCondition); }">🗑</span>
</div>
</div>
<div class="control-group" data-bind="visible: 0 === conditions().length"
data-i18n="POPUPS_FILTER/ALL_INCOMING_MESSAGES_DESC"></div>
<div class="control-group">
<a class="btn" data-bind="click: addCondition, i18nInit: true" data-icon="✚" data-i18n="POPUPS_FILTER/BUTTON_ADD_CONDITION"></a>
</div>
<div class="legend" data-i18n="POPUPS_FILTER/LEGEND_ACTIONS"></div>
<select class="span3" data-bind="options: $root.actionTypeOptions, value: actionType, optionsText: 'name', optionsValue: 'id'"></select>
<div data-bind="template: {'name': actionTemplate()}, i18nUpdate: actionTemplate"></div>
</div>
<footer>
<a class="btn buttonSave" data-bind="click: saveFilter" data-icon="✔" data-i18n="GLOBAL/DONE"></a>

View file

@ -1,5 +1,5 @@
<!-- ko with: script -->
<header>
<header data-bind="i18nInit: true">
<a href="#" class="close" data-bind="click: $root.close">×</a>
<h3>
<span data-i18n="POPUPS_SIEVE_SCRIPT/TITLE_CREATE" data-bind="visible: !exists()"></span>
@ -7,7 +7,7 @@
<span data-bind="visible: exists">: <!-- ko text: name--><!-- /ko --></span>
</h3>
</header>
<div class="modal-body">
<div class="modal-body" data-bind="i18nInit: true">
<div class="control-group" data-bind="css: {'error': nameError}, hidden: exists">
<input type="text" class="span5"
@ -60,7 +60,7 @@
<a class="btn" data-bind="click: $root.addFilter" data-icon="✚" data-i18n="POPUPS_SIEVE_SCRIPT/BUTTON_ADD_FILTER"></a>
</div>
</div>
<footer>
<footer data-bind="i18nInit: true">
<a class="btn" data-bind="visible: $root.allowToggle, click: function() { $root.toggleFiltersRaw(); }, css: {'active': $root.rawActive }" data-i18n="[title]POPUPS_SIEVE_SCRIPT/BUTTON_RAW_SCRIPT">
<i class="icon-file-code"></i>
</a>

View file

@ -30,4 +30,4 @@
</tr>
</tbody>
</table>
<a class="btn" data-bind="click: $root.addScript" data-icon="✚" data-i18n="SETTINGS_FILTERS/BUTTON_ADD_SCRIPT"></a>
<a class="btn" data-bind="click: addScript" data-icon="✚" data-i18n="SETTINGS_FILTERS/BUTTON_ADD_SCRIPT"></a>

View file

@ -133,6 +133,6 @@ exports.jsLint = jsLint;
exports.js = gulp.series(
jsClean,
jsLint,
gulp.parallel(jsBoot, jsServiceWorker, jsOpenPGP, jsLibs/*, jsSieve*/, jsApp, jsAdmin),
gulp.parallel(jsBoot, jsServiceWorker, jsOpenPGP, jsLibs, jsSieve, jsApp, jsAdmin),
jsMin
);