From e66c888b86a93829495de1529c4d3f3bda46c209 Mon Sep 17 00:00:00 2001 From: Stoiko Ivanov Date: Thu, 27 Oct 2022 21:12:57 +0200 Subject: [PATCH] quarantine: refactor spamquarantine controller over the time the spam quarantine has gained quite a few nice usability improvments and features, which would be useful in the virus and attachment quarantines as well - e.g.: * multi-mail selection * keyboard actions * context menu * download mail as .eml this patch splits the controller part into a file of its own, while changing 'var' to 'let' and removing the parts only relevant for the spamquarantine and adapts the current SpamQuarantine.js to use it. Signed-off-by: Stoiko Ivanov --- js/Makefile | 1 + js/SpamQuarantine.js | 290 +++++++++----------------- js/controller/QuarantineController.js | 170 +++++++++++++++ 3 files changed, 265 insertions(+), 196 deletions(-) create mode 100644 js/controller/QuarantineController.js diff --git a/js/Makefile b/js/Makefile index b904598..9a2bcf2 100644 --- a/js/Makefile +++ b/js/Makefile @@ -19,6 +19,7 @@ JSSRC= \ RuleInfo.js \ RuleEditor.js \ MainView.js \ + controller/QuarantineController.js \ QuarantineContextMenu.js \ QuarantineList.js \ SpamInfoGrid.js \ diff --git a/js/SpamQuarantine.js b/js/SpamQuarantine.js index 6fe20d7..cf7f181 100644 --- a/js/SpamQuarantine.js +++ b/js/SpamQuarantine.js @@ -33,225 +33,123 @@ Ext.define('pmg-spam-list', { idProperty: 'id', }); -Ext.define('PMG.SpamQuarantine', { - extend: 'Ext.container.Container', - xtype: 'pmgSpamQuarantine', - - border: false, - layout: { type: 'border' }, +Ext.define('PMG.SpamQuarantineController', { + extend: 'PMG.controller.QuarantineController', + xtype: 'pmgSpamQuarantineController', + alias: 'controller.spamquarantine', - defaults: { border: false }, + updatePreview: function(raw, rec) { + let me = this; + me.lookupReference('spam').setDisabled(false); - // from mail link - cselect: undefined, + me.callParent(arguments); + }, - viewModel: { - parent: null, - data: { - mailid: '', - }, - formulas: { - downloadMailURL: get => '/api2/json/quarantine/download?mailid=' + encodeURIComponent(get('mailid')), - }, + multiSelect: function(selection) { + let me = this; + let spam = me.lookupReference('spam'); + spam.setDisabled(true); + spam.setPressed(false); + me.lookupReference('spaminfo').setVisible(false); + me.callParent(selection); }, - controller: { - xclass: 'Ext.app.ViewController', + onSelectMail: function() { + let me = this; + let list = me.lookupReference('list'); + let selection = list.selModel.getSelection(); + if (selection.length <= 1) { + let rec = selection[0] || {}; + me.lookupReference('spaminfo').setID(rec); + } + me.callParent(); + }, - updatePreview: function(raw, rec) { - var preview = this.lookupReference('preview'); - if (!rec || !rec.data || !rec.data.id) { - preview.update(''); - preview.setDisabled(true); - return; - } + toggleSpamInfo: function(btn) { + var grid = this.lookupReference('spaminfo'); + grid.setVisible(!grid.isVisible()); + }, - let url = `/api2/htmlmail/quarantine/content?id=${rec.data.id}`; - if (raw) { - url += '&raw=1'; - } - preview.setDisabled(false); - this.lookupReference('raw').setDisabled(false); - this.lookupReference('spam').setDisabled(false); - this.lookupReference('download').setDisabled(false); - preview.update(""); - }, + openContextMenu: function(table, record, tr, index, event) { + event.stopEvent(); + let me = this; + let list = me.lookup('list'); + Ext.create('PMG.menu.SpamContextMenu', { + callback: action => me.doAction(action, list.getSelection()), + }).showAt(event.getXY()); + }, - multiSelect: function(selection) { - var preview = this.lookupReference('preview'); - var raw = this.lookupReference('raw'); - var spam = this.lookupReference('spam'); - var spaminfo = this.lookupReference('spaminfo'); - var mailinfo = this.lookupReference('mailinfo'); - var download = this.lookupReference('download'); + keyPress: function(table, record, item, index, event) { + var me = this; + var list = me.lookup('list'); + var key = event.getKey(); + var action = ''; + switch (key) { + case event.DELETE: + case 127: + action = 'delete'; + break; + case Ext.event.Event.D: + case Ext.event.Event.D + 32: + action = 'deliver'; + break; + case Ext.event.Event.W: + case Ext.event.Event.W + 32: + action = 'whitelist'; + break; + case Ext.event.Event.B: + case Ext.event.Event.B + 32: + action = 'blacklist'; + break; + } - preview.setDisabled(false); - preview.update(`

${gettext('Multiple E-Mails selected')} (${selection.length})

`); - raw.setDisabled(true); - spam.setDisabled(true); - spam.setPressed(false); - spaminfo.setVisible(false); - mailinfo.setVisible(false); - download.setDisabled(true); - }, + if (action !== '') { + me.doAction(action, list.getSelection()); + } + }, - toggleRaw: function(button) { - var me = this; - var list = me.lookupReference('list'); - var rec = list.selModel.getSelection()[0]; - me.lookupReference('mailinfo').setVisible(me.raw); - me.raw = !me.raw; - me.updatePreview(me.raw, rec); - }, + init: function(view) { + this.lookup('list').cselect = view.cselect; + }, - btnHandler: function(button, e) { - var me = this; - var action = button.reference; - var list = this.lookupReference('list'); - var selected = list.getSelection(); - me.doAction(action, selected); + control: { + 'button[reference=raw]': { + click: 'toggleRaw', }, - - doAction: function(action, selected) { - if (!selected.length) { - return; - } - - var list = this.lookupReference('list'); - - if (selected.length > 1) { - let idlist = selected.map(item => item.data.id); - Ext.Msg.confirm( - gettext('Confirm'), - Ext.String.format( - gettext("Action '{0}' for '{1}' items"), - action, selected.length, - ), - async function(button) { - if (button !== 'yes') { - return; - } - - list.mask(gettext('Processing...'), 'x-mask-loading'); - - const sliceSize = 2500, maxInFlight = 2; - let batches = [], batchCount = Math.ceil(selected.length / sliceSize); - for (let i = 0; i * sliceSize < selected.length; i++) { - let sliceStart = i * sliceSize; - let sliceEnd = Math.min(sliceStart + sliceSize, selected.length); - batches.push( - PMG.Async.doQAction( - action, - idlist.slice(sliceStart, sliceEnd), - i + 1, - batchCount, - ), - ); - if (batches.length >= maxInFlight) { - await Promise.allSettled(batches); // eslint-disable-line no-await-in-loop - batches = []; - } - } - await Promise.allSettled(batches); // await possible remaining ones - list.unmask(); - // below can be slow, we could remove directly from the in-memory store, but - // with lots of elements and some failures we could be quite out of sync? - list.getController().load(); - }, - ); - return; - } - - PMG.Utils.doQuarantineAction(action, selected[0].data.id, function() { - let listController = list.getController(); - listController.allowPositionSave = false; - // success -> remove directly to avoid slow store reload for a single-element action - list.getStore().remove(selected[0]); - listController.restoreSavedSelection(); - listController.allowPositionSave = true; - }); + 'button[reference=spam]': { + click: 'toggleSpamInfo', }, - - onSelectMail: function() { - var me = this; - var list = this.lookupReference('list'); - var selection = list.selModel.getSelection(); - if (selection.length > 1) { - me.multiSelect(selection); - return; - } - - var rec = selection[0] || {}; - - me.getViewModel().set('mailid', rec.data ? rec.data.id : ''); - me.updatePreview(me.raw || false, rec); - me.lookupReference('spaminfo').setID(rec); - me.lookupReference('mailinfo').setVisible(!!rec.data && !me.raw); - me.lookupReference('mailinfo').update(rec.data); + 'pmgQuarantineList': { + selectionChange: 'onSelectMail', + itemkeypress: 'keyPress', + rowcontextmenu: 'openContextMenu', }, + }, +}); - toggleSpamInfo: function(btn) { - var grid = this.lookupReference('spaminfo'); - grid.setVisible(!grid.isVisible()); - }, +Ext.define('PMG.SpamQuarantine', { + extend: 'Ext.container.Container', + xtype: 'pmgSpamQuarantine', - openContextMenu: function(table, record, tr, index, event) { - event.stopEvent(); - let me = this; - let list = me.lookup('list'); - Ext.create('PMG.menu.SpamContextMenu', { - callback: action => me.doAction(action, list.getSelection()), - }).showAt(event.getXY()); - }, + border: false, + layout: { type: 'border' }, - keyPress: function(table, record, item, index, event) { - var me = this; - var list = me.lookup('list'); - var key = event.getKey(); - var action = ''; - switch (key) { - case event.DELETE: - case 127: - action = 'delete'; - break; - case Ext.event.Event.D: - case Ext.event.Event.D + 32: - action = 'deliver'; - break; - case Ext.event.Event.W: - case Ext.event.Event.W + 32: - action = 'whitelist'; - break; - case Ext.event.Event.B: - case Ext.event.Event.B + 32: - action = 'blacklist'; - break; - } + defaults: { border: false }, - if (action !== '') { - me.doAction(action, list.getSelection()); - } - }, + // from mail link + cselect: undefined, - init: function(view) { - this.lookup('list').cselect = view.cselect; + viewModel: { + parent: null, + data: { + mailid: '', }, - - control: { - 'button[reference=raw]': { - click: 'toggleRaw', - }, - 'button[reference=spam]': { - click: 'toggleSpamInfo', - }, - 'pmgQuarantineList': { - selectionChange: 'onSelectMail', - itemkeypress: 'keyPress', - rowcontextmenu: 'openContextMenu', - }, + formulas: { + downloadMailURL: get => '/api2/json/quarantine/download?mailid=' + encodeURIComponent(get('mailid')), }, }, + controller: 'spamquarantine', items: [ { diff --git a/js/controller/QuarantineController.js b/js/controller/QuarantineController.js new file mode 100644 index 0000000..dfe2915 --- /dev/null +++ b/js/controller/QuarantineController.js @@ -0,0 +1,170 @@ +Ext.define('PMG.controller.QuarantineController', { + extend: 'Ext.app.ViewController', + xtype: 'controller.Quarantine', + alias: 'controller.quarantine', + + updatePreview: function(raw, rec) { + let preview = this.lookupReference('preview'); + + if (!rec || !rec.data || !rec.data.id) { + preview.update(''); + preview.setDisabled(true); + return; + } + + let url = `/api2/htmlmail/quarantine/content?id=${rec.data.id}`; + if (raw) { + url += '&raw=1'; + } + preview.setDisabled(false); + this.lookupReference('raw').setDisabled(false); + this.lookupReference('download').setDisabled(false); + preview.update(""); + }, + + multiSelect: function(selection) { + let me = this; + me.lookupReference('raw').setDisabled(true); + me.lookupReference('download').setDisabled(true); + me.lookupReference('mailinfo').setVisible(false); + + let preview = me.lookupReference('preview'); + preview.setDisabled(false); + preview.update(`

${gettext('Multiple E-Mails selected')} (${selection.length})

`); + }, + + toggleRaw: function(button) { + let me = this; + let list = me.lookupReference('list'); + let rec = list.selModel.getSelection()[0]; + me.lookupReference('mailinfo').setVisible(me.raw); + me.raw = !me.raw; + me.updatePreview(me.raw, rec); + }, + + btnHandler: function(button, e) { + let me = this; + let action = button.reference; + let list = me.lookupReference('list'); + let selected = list.getSelection(); + me.doAction(action, selected); + }, + + doAction: function(action, selected) { + if (!selected.length) { + return; + } + + let list = this.lookupReference('list'); + + if (selected.length > 1) { + let idlist = selected.map(item => item.data.id); + Ext.Msg.confirm( + gettext('Confirm'), + Ext.String.format( + gettext("Action '{0}' for '{1}' items"), + action, selected.length, + ), + async function(button) { + if (button !== 'yes') { + return; + } + + list.mask(gettext('Processing...'), 'x-mask-loading'); + + const sliceSize = 2500, maxInFlight = 2; + let batches = [], batchCount = Math.ceil(selected.length / sliceSize); + for (let i = 0; i * sliceSize < selected.length; i++) { + let sliceStart = i * sliceSize; + let sliceEnd = Math.min(sliceStart + sliceSize, selected.length); + batches.push( + PMG.Async.doQAction( + action, + idlist.slice(sliceStart, sliceEnd), + i + 1, + batchCount, + ), + ); + if (batches.length >= maxInFlight) { + await Promise.allSettled(batches); // eslint-disable-line no-await-in-loop + batches = []; + } + } + await Promise.allSettled(batches); // await possible remaining ones + list.unmask(); + // below can be slow, we could remove directly from the in-memory store, but + // with lots of elements and some failures we could be quite out of sync? + list.getController().load(); + }, + ); + return; + } + + PMG.Utils.doQuarantineAction(action, selected[0].data.id, function() { + let listController = list.getController(); + listController.allowPositionSave = false; + // success -> remove directly to avoid slow store reload for a single-element action + list.getStore().remove(selected[0]); + listController.restoreSavedSelection(); + listController.allowPositionSave = true; + }); + }, + + onSelectMail: function() { + let me = this; + let list = this.lookupReference('list'); + let selection = list.selModel.getSelection(); + if (selection.length > 1) { + me.multiSelect(selection); + return; + } + + let rec = selection[0] || {}; + + me.getViewModel().set('mailid', rec.data ? rec.data.id : ''); + me.updatePreview(me.raw || false, rec); + me.lookupReference('mailinfo').setVisible(!!rec.data && !me.raw); + me.lookupReference('mailinfo').update(rec.data); + }, + + openContextMenu: function(table, record, tr, index, event) { + event.stopEvent(); + let me = this; + let list = me.lookup('list'); + Ext.create('PMG.menu.QuarantineContextMenu', { + callback: action => me.doAction(action, list.getSelection()), + }).showAt(event.getXY()); + }, + + keyPress: function(table, record, item, index, event) { + let me = this; + let list = me.lookup('list'); + let key = event.getKey(); + let action = ''; + switch (key) { + case event.DELETE: + case 127: + action = 'delete'; + break; + case Ext.event.Event.D: + case Ext.event.Event.D + 32: + action = 'deliver'; + break; + } + + if (action !== '') { + me.doAction(action, list.getSelection()); + } + }, + + control: { + 'button[reference=raw]': { + click: 'toggleRaw', + }, + 'pmgQuarantineList': { + selectionChange: 'onSelectMail', + itemkeypress: 'keyPress', + rowcontextmenu: 'openContextMenu', + }, + }, +}); -- 2.39.2