mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-07 00:17:03 +03:00
Merge pull request #1673 from AbdoBnHesham/master
Add Search Filters Plugin with Gmail-like Functionality
This commit is contained in:
commit
85fb2ff44d
6 changed files with 724 additions and 0 deletions
211
plugins/search-filters/index.php
Normal file
211
plugins/search-filters/index.php
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class SearchFiltersPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
{
|
||||||
|
public const
|
||||||
|
NAME = 'Search Filters',
|
||||||
|
VERSION = '0.0.2',
|
||||||
|
RELEASE = '2024-06-28',
|
||||||
|
REQUIRED = '2.36.3',
|
||||||
|
CATEGORY = 'General',
|
||||||
|
LICENSE = 'MIT',
|
||||||
|
DESCRIPTION = 'Add filters to search queries';
|
||||||
|
|
||||||
|
public function Init(): void
|
||||||
|
{
|
||||||
|
$this->UseLangs(true);
|
||||||
|
|
||||||
|
$this->addHook('imap.after-login', 'ApplyFilters');
|
||||||
|
|
||||||
|
$this->addTemplate('templates/STabSearchFilters.html');
|
||||||
|
|
||||||
|
$this->addTemplate('templates/PopupsSearchFilters.html');
|
||||||
|
$this->addTemplate('templates/PopupsSTabAdvancedSearch.html');
|
||||||
|
$this->addJs('js/SearchFilters.js');
|
||||||
|
|
||||||
|
$this->addJsonHook('SGetFilters', 'GetFilters');
|
||||||
|
$this->addJsonHook('SAddEditFilter', 'AddEditFilter');
|
||||||
|
$this->addJsonHook('SUpdateSearchQ', 'UpdateSearchQ');
|
||||||
|
$this->addJsonHook('SDeleteFilter', 'DeleteFilter');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ApplyFilters(
|
||||||
|
\RainLoop\Model\Account $oAccount,
|
||||||
|
\MailSo\Imap\ImapClient $oImapClient,
|
||||||
|
bool $bSuccess,
|
||||||
|
\MailSo\Imap\Settings $oSettings
|
||||||
|
) {
|
||||||
|
if (!$bSuccess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$aSettings = $this->getUserSettings();
|
||||||
|
if (empty($aSettings['SFilters'])) {
|
||||||
|
$aSettings['SFilters'] = [];
|
||||||
|
$this->saveUserSettings($aSettings);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$Filters = $aSettings['SFilters'];
|
||||||
|
|
||||||
|
foreach ($Filters as $filter) {
|
||||||
|
$this->Manager()->logWrite(json_encode([
|
||||||
|
'filter' => $filter,
|
||||||
|
]), LOG_WARNING);
|
||||||
|
|
||||||
|
$folder = 'INBOX';
|
||||||
|
$searchQ = $filter['searchQ'];
|
||||||
|
$uids = $this->searchMessages($oImapClient, $searchQ, "INBOX");
|
||||||
|
|
||||||
|
//Mark as read/seen
|
||||||
|
if ($filter['fSeen']) {
|
||||||
|
foreach ($uids as $uid) {
|
||||||
|
$oRange = new MailSo\Imap\SequenceSet([$uid]);
|
||||||
|
$this->Manager()->Actions()->MailClient()->MessageSetFlag(
|
||||||
|
$folder,
|
||||||
|
$oRange,
|
||||||
|
MailSo\Imap\Enumerations\MessageFlag::SEEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Flag/Star message
|
||||||
|
if ($filter['fFlag']) {
|
||||||
|
foreach ($uids as $uid) {
|
||||||
|
$oRange = new MailSo\Imap\SequenceSet([$uid]);
|
||||||
|
$this->Manager()->Actions()->MailClient()->MessageSetFlag(
|
||||||
|
$folder,
|
||||||
|
$oRange,
|
||||||
|
MailSo\Imap\Enumerations\MessageFlag::FLAGGED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to folder
|
||||||
|
if ($filter['fFolder']) {
|
||||||
|
$folder = $filter['fFolder'];
|
||||||
|
foreach ($uids as $uid) {
|
||||||
|
$oRange = new MailSo\Imap\SequenceSet([$uid]);
|
||||||
|
$oImapClient->MessageMove("INBOX", $folder, $oRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function searchMessages(
|
||||||
|
\MailSo\Imap\ImapClient $imapClient,
|
||||||
|
string $search,
|
||||||
|
string $folder = "INBOX"
|
||||||
|
): array {
|
||||||
|
$oParams = new \MailSo\Mail\MessageListParams();
|
||||||
|
$oParams->sSearch = $search;
|
||||||
|
$oParams->sFolderName = $folder;
|
||||||
|
|
||||||
|
$bUseCache = false;
|
||||||
|
$oSearchCriterias = \MailSo\Imap\SearchCriterias::fromString(
|
||||||
|
$imapClient,
|
||||||
|
$folder,
|
||||||
|
$search,
|
||||||
|
true,
|
||||||
|
$bUseCache
|
||||||
|
);
|
||||||
|
|
||||||
|
$imapClient->FolderSelect($folder);
|
||||||
|
return $imapClient->MessageSearch($oSearchCriterias, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetFilters()
|
||||||
|
{
|
||||||
|
$aSettings = $this->getUserSettings();
|
||||||
|
$Filters = $aSettings['SFilters'] ?? [];
|
||||||
|
|
||||||
|
$Search = $this->jsonParam('SSearchQ');
|
||||||
|
if (!$Search) {
|
||||||
|
return $this->jsonResponse(__FUNCTION__, ['SFilters' => $Filters]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$Filter = null;
|
||||||
|
foreach ($aSettings['SFilters'] as $filter) {
|
||||||
|
if ($filter['searchQ'] == $Search) {
|
||||||
|
$Filter = $filter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->jsonResponse(__FUNCTION__, ['SFilter' => $Filter]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function AddEditFilter()
|
||||||
|
{
|
||||||
|
$SFilter = $this->jsonParam('SFilter');
|
||||||
|
$newFilter = [
|
||||||
|
'searchQ' => $SFilter['searchQ'],
|
||||||
|
'priority' => $SFilter['priority'] ?? 1,
|
||||||
|
'fFolder' => $SFilter['fFolder'],
|
||||||
|
'fSeen' => $SFilter['fSeen'],
|
||||||
|
'fFlag' => $SFilter['fFlag'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$aSettings = $this->getUserSettings();
|
||||||
|
$aSettings['SFilters'] = $aSettings['SFilters'] ?? [];
|
||||||
|
|
||||||
|
$foundIndex = null;
|
||||||
|
foreach ($aSettings['SFilters'] as $index => $filter) {
|
||||||
|
if ($filter['searchQ'] == $SFilter['searchQ']) {
|
||||||
|
if ($filter['priority'] != $SFilter['priority']) {
|
||||||
|
array_splice($aSettings['SFilters'], $index, 1);
|
||||||
|
} else {
|
||||||
|
$foundIndex = $index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($foundIndex === null) {
|
||||||
|
$insertIndex = 0;
|
||||||
|
foreach ($aSettings['SFilters'] as $index => $filter)
|
||||||
|
if ($filter['priority'] >= $newFilter['priority'])
|
||||||
|
$insertIndex = $index + 1;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
|
||||||
|
array_splice($aSettings['SFilters'], $insertIndex, 0, [$newFilter]);
|
||||||
|
} else {
|
||||||
|
$aSettings['SFilters'][$foundIndex] = $newFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings($aSettings));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function UpdateSearchQ()
|
||||||
|
{
|
||||||
|
$SFilter = $this->jsonParam('SFilter');
|
||||||
|
|
||||||
|
$aSettings = $this->getUserSettings();
|
||||||
|
$aSettings['SFilters'] = $aSettings['SFilters'] ?? [];
|
||||||
|
|
||||||
|
foreach ($aSettings['SFilters'] as $index => $filter) {
|
||||||
|
if ($filter['searchQ'] == $SFilter['oldSearchQ']) {
|
||||||
|
$filter['searchQ'] = $SFilter['searchQ'];
|
||||||
|
$aSettings['SFilters'][$index] = $filter;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings($aSettings));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function DeleteFilter()
|
||||||
|
{
|
||||||
|
$Search = $this->jsonParam('SSearchQ');
|
||||||
|
|
||||||
|
$aSettings = $this->getUserSettings();
|
||||||
|
$aSettings['SFilters'] = $aSettings['SFilters'] ?? [];
|
||||||
|
|
||||||
|
foreach ($aSettings['SFilters'] as $index => $filter) {
|
||||||
|
if ($filter['searchQ'] == $Search) {
|
||||||
|
array_splice($aSettings['SFilters'], $index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings($aSettings));
|
||||||
|
}
|
||||||
|
}
|
||||||
317
plugins/search-filters/js/SearchFilters.js
Normal file
317
plugins/search-filters/js/SearchFilters.js
Normal file
|
|
@ -0,0 +1,317 @@
|
||||||
|
((rl) => {
|
||||||
|
const EmptyOption = { id: -1, name: '' };
|
||||||
|
const Folders = ko.computed(() => {
|
||||||
|
return [EmptyOption, ...rl.app.folderList().map((f) => ({ name: f.name }))];
|
||||||
|
});
|
||||||
|
const Priorities = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||||
|
const searchQ = ko.observable(''),
|
||||||
|
priority = ko.observable(1),
|
||||||
|
oldSearchQ = ko.observable(''),
|
||||||
|
fFolder = ko.observable(''),
|
||||||
|
fSeen = ko.observable(false),
|
||||||
|
fFlag = ko.observable(false),
|
||||||
|
ioFilters = ko.observableArray([]);
|
||||||
|
|
||||||
|
const i18n = (val) => rl.i18n(`SFILTERS/${val}`);
|
||||||
|
|
||||||
|
const ServerActions = {
|
||||||
|
GetFilters(loading) {
|
||||||
|
ioFilters([]);
|
||||||
|
rl.pluginRemoteRequest((iError, oData) => {
|
||||||
|
if (iError) return console.error(iError);
|
||||||
|
|
||||||
|
oData.Result.SFilters.forEach((f) => ioFilters.push(ko.observable(f)));
|
||||||
|
|
||||||
|
if (loading) loading(false);
|
||||||
|
}, 'SGetFilters');
|
||||||
|
},
|
||||||
|
GetFilter() {
|
||||||
|
rl.pluginRemoteRequest(
|
||||||
|
(iError, oData) => {
|
||||||
|
if (iError) return console.error(iError);
|
||||||
|
|
||||||
|
const filter = oData.Result.SFilter || {};
|
||||||
|
priority(filter.priority || 1);
|
||||||
|
fFolder(filter.fFolder);
|
||||||
|
fSeen(filter.fSeen);
|
||||||
|
fFlag(filter.fFlag);
|
||||||
|
},
|
||||||
|
'SGetFilters',
|
||||||
|
{
|
||||||
|
SSearchQ: searchQ()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
AddOrEditFilter() {
|
||||||
|
rl.pluginRemoteRequest(
|
||||||
|
(iError, oData) => {
|
||||||
|
if (!iError) this.GetFilters();
|
||||||
|
},
|
||||||
|
'SAddEditFilter',
|
||||||
|
{
|
||||||
|
SFilter: {
|
||||||
|
searchQ: oldSearchQ() || searchQ(),
|
||||||
|
priority: priority(),
|
||||||
|
fFolder: fFolder(),
|
||||||
|
fSeen: fSeen(),
|
||||||
|
fFlag: fFlag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
UpdateSearchQ(newSearchQ) {
|
||||||
|
rl.pluginRemoteRequest(
|
||||||
|
(iError, oData) => {
|
||||||
|
if (!iError) this.GetFilters();
|
||||||
|
},
|
||||||
|
'SUpdateSearchQ',
|
||||||
|
{
|
||||||
|
SFilter: {
|
||||||
|
oldSearchQ: oldSearchQ(),
|
||||||
|
searchQ: newSearchQ
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
RemoveFilter(searchQToRemove) {
|
||||||
|
rl.pluginRemoteRequest(
|
||||||
|
(iError, oData) => {
|
||||||
|
if (iError) return console.error(iError, oData);
|
||||||
|
const index = ioFilters().findIndex((f) => f.searchQ() === searchQToRemove);
|
||||||
|
ioFilters.splice(index, 1);
|
||||||
|
},
|
||||||
|
'SDeleteFilter',
|
||||||
|
{
|
||||||
|
SSearchQ: searchQToRemove
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventListener('rl-view-model', (event) => {
|
||||||
|
const advS = event.detail;
|
||||||
|
if (advS.viewModelTemplateID == 'PopupsAdvancedSearch') {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.setAttribute('data-i18n', 'SFILTERS/CREATE_FILTER');
|
||||||
|
button.addEventListener('click', function () {
|
||||||
|
searchQ(advS.buildSearchString());
|
||||||
|
if (searchQ()) SearchFiltersPopupView.showModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
const footer = advS.querySelector('footer');
|
||||||
|
footer.style.display = 'flex';
|
||||||
|
footer.style.justifyContent = 'space-between';
|
||||||
|
|
||||||
|
footer.prepend(button);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class SearchFiltersSettingsTab {
|
||||||
|
constructor() {
|
||||||
|
this.folders = Folders;
|
||||||
|
this.Priorities = Priorities;
|
||||||
|
this.ioFilters = ioFilters;
|
||||||
|
|
||||||
|
this.loading = ko.observable(false);
|
||||||
|
this.saving = ko.observable(false);
|
||||||
|
|
||||||
|
this.i18n = i18n;
|
||||||
|
|
||||||
|
this.savingOrLoading = ko.computed(() => {
|
||||||
|
return this.loading() || this.saving();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
edit(filter) {
|
||||||
|
oldSearchQ(filter.searchQ);
|
||||||
|
searchQ(filter.searchQ);
|
||||||
|
priority(filter.priority || 1);
|
||||||
|
fFolder(filter.fFolder);
|
||||||
|
fSeen(filter.fSeen);
|
||||||
|
fFlag(filter.fFlag);
|
||||||
|
AdvancedSearchPopupView.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(filter) {
|
||||||
|
ServerActions.RemoveFilter(filter.searchQ);
|
||||||
|
}
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
this.clear();
|
||||||
|
this.loading(true);
|
||||||
|
ServerActions.GetFilters(this.loading);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.ioFilters([]);
|
||||||
|
this.loading(false);
|
||||||
|
this.saving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.addSettingsViewModel(SearchFiltersSettingsTab, 'STabSearchFilters', 'Filters', 'filters');
|
||||||
|
|
||||||
|
class SearchFiltersPopupView extends rl.pluginPopupView {
|
||||||
|
constructor() {
|
||||||
|
super('SearchFilters');
|
||||||
|
|
||||||
|
this.folders = Folders;
|
||||||
|
this.Priorities = Priorities;
|
||||||
|
|
||||||
|
this.priority = priority;
|
||||||
|
this.fFolder = fFolder;
|
||||||
|
this.fSeen = fSeen;
|
||||||
|
this.fFlag = fFlag;
|
||||||
|
|
||||||
|
this.usefFolder = ko.observable(false);
|
||||||
|
this.fFolder.subscribe((v) => this.usefFolder(!!v));
|
||||||
|
}
|
||||||
|
|
||||||
|
submitForm() {
|
||||||
|
if (!this.usefFolder()) fFolder('');
|
||||||
|
ServerActions.AddOrEditFilter();
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeShow() {
|
||||||
|
if (!oldSearchQ()) {
|
||||||
|
ServerActions.GetFilter();
|
||||||
|
} else {
|
||||||
|
this.usefFolder(!!fFolder());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AdvancedSearchPopupView extends rl.pluginPopupView {
|
||||||
|
constructor() {
|
||||||
|
super('STabAdvancedSearch');
|
||||||
|
|
||||||
|
this.addObservables({
|
||||||
|
from: '',
|
||||||
|
to: '',
|
||||||
|
subject: '',
|
||||||
|
text: '',
|
||||||
|
repliedValue: -1,
|
||||||
|
selectedDateValue: -1,
|
||||||
|
selectedTreeValue: '',
|
||||||
|
|
||||||
|
hasAttachment: false,
|
||||||
|
starred: false,
|
||||||
|
unseen: false
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addComputables({
|
||||||
|
repliedOptions: () => {
|
||||||
|
return [
|
||||||
|
{ id: -1, name: '' },
|
||||||
|
{ id: 1, name: rl.i18n('GLOBAL/YES') },
|
||||||
|
{ id: 0, name: rl.i18n('GLOBAL/NO') }
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedDates: () => {
|
||||||
|
let prefix = 'SEARCH/DATE_';
|
||||||
|
return [
|
||||||
|
{ id: -1, name: rl.i18n(prefix + 'ALL') },
|
||||||
|
{ id: 3, name: rl.i18n(prefix + '3_DAYS') },
|
||||||
|
{ id: 7, name: rl.i18n(prefix + '7_DAYS') },
|
||||||
|
{ id: 30, name: rl.i18n(prefix + 'MONTH') },
|
||||||
|
{ id: 90, name: rl.i18n(prefix + '3_MONTHS') },
|
||||||
|
{ id: 180, name: rl.i18n(prefix + '6_MONTHS') },
|
||||||
|
{ id: 365, name: rl.i18n(prefix + 'YEAR') }
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedTree: () => {
|
||||||
|
let prefix = 'SEARCH/SUBFOLDERS_';
|
||||||
|
return [
|
||||||
|
{ id: '', name: rl.i18n(prefix + 'NONE') },
|
||||||
|
{ id: 'subtree-one', name: rl.i18n(prefix + 'SUBTREE_ONE') },
|
||||||
|
{ id: 'subtree', name: rl.i18n(prefix + 'SUBTREE') }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
submitForm() {
|
||||||
|
const newSearchQ = this.buildSearchString();
|
||||||
|
if (newSearchQ != oldSearchQ()) ServerActions.UpdateSearchQ(newSearchQ);
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
editFilters() {
|
||||||
|
SearchFiltersPopupView.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSearchString() {
|
||||||
|
const self = this,
|
||||||
|
data = new FormData(),
|
||||||
|
append = (key, value) => value.length && data.append(key, value);
|
||||||
|
|
||||||
|
append('from', self.from().trim());
|
||||||
|
append('to', self.to().trim());
|
||||||
|
append('subject', self.subject().trim());
|
||||||
|
append('text', self.text().trim());
|
||||||
|
append('in', self.selectedTreeValue());
|
||||||
|
if (-1 < self.selectedDateValue()) {
|
||||||
|
let d = new Date();
|
||||||
|
d.setDate(d.getDate() - self.selectedDateValue());
|
||||||
|
append('since', d.toISOString().split('T')[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = decodeURIComponent(new URLSearchParams(data).toString());
|
||||||
|
|
||||||
|
if (self.hasAttachment()) {
|
||||||
|
result += '&attachment';
|
||||||
|
}
|
||||||
|
if (self.unseen()) {
|
||||||
|
result += '&unseen';
|
||||||
|
}
|
||||||
|
if (self.starred()) {
|
||||||
|
result += '&flagged';
|
||||||
|
}
|
||||||
|
if (1 == self.repliedValue()) {
|
||||||
|
result += '&answered';
|
||||||
|
}
|
||||||
|
if (0 == self.repliedValue()) {
|
||||||
|
result += '&unanswered';
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.replace(/^&+/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
const pString = (value) => (null != value ? '' + value : '');
|
||||||
|
|
||||||
|
const self = this,
|
||||||
|
params = new URLSearchParams('?' + searchQ());
|
||||||
|
self.from(pString(params.get('from')));
|
||||||
|
self.to(pString(params.get('to')));
|
||||||
|
self.subject(pString(params.get('subject')));
|
||||||
|
self.text(pString(params.get('text')));
|
||||||
|
self.selectedTreeValue(pString(params.get('in')));
|
||||||
|
self.selectedDateValue(-1);
|
||||||
|
self.hasAttachment(params.has('attachment'));
|
||||||
|
self.starred(params.has('flagged'));
|
||||||
|
self.unseen(params.has('unseen'));
|
||||||
|
if (params.has('answered')) {
|
||||||
|
self.repliedValue(1);
|
||||||
|
} else if (params.has('unanswered')) {
|
||||||
|
self.repliedValue(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
oldSearchQ('');
|
||||||
|
searchQ('');
|
||||||
|
fFolder('');
|
||||||
|
priority(1);
|
||||||
|
fSeen(false);
|
||||||
|
fFlag(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onHide() {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(window.rl);
|
||||||
21
plugins/search-filters/langs/en.json
Normal file
21
plugins/search-filters/langs/en.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"SFILTERS": {
|
||||||
|
"WHEN_MESSAGE_MATCH_SEARCH_CRITERIA": "When a message is an exact match for your search criteria:",
|
||||||
|
"FILTERS": "Filters",
|
||||||
|
"FILTER": "Filter",
|
||||||
|
"MOVE_TO_FOLDER": "Move to folder: ",
|
||||||
|
"CREATE_FILTER": "Create filter",
|
||||||
|
"EDIT_FILTERS": "Edit filters",
|
||||||
|
"MATCHES": "Matches: ",
|
||||||
|
"DO_THIS": "Do this: ",
|
||||||
|
"SAVE": "Save",
|
||||||
|
"EDIT": "Edit",
|
||||||
|
"REMOVE": "Remove",
|
||||||
|
"NO_FILTERS": "You haven't added any filters yet.",
|
||||||
|
"THE_FOLLOWING_FILTERS_ARE_APPLIED": "The following filters are applied to all incoming mail: ",
|
||||||
|
"MARK_AS_READ": "Mark as read",
|
||||||
|
"STAR_IT": "Star it",
|
||||||
|
"PRIORITY_LABLE": "Priority points, Higher means it'll apply first",
|
||||||
|
"PRIORITY_POINTS": "Priority points"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
<header>
|
||||||
|
<a href="#" class="close" data-bind="click: close">×</a>
|
||||||
|
<h3 data-i18n="SEARCH/TITLE_ADV"></h3>
|
||||||
|
</header>
|
||||||
|
<form id="advsearchform" class="modal-body form-horizontal" action="#/" autocomplete="off" spellcheck="false"
|
||||||
|
data-bind="submit: submitForm">
|
||||||
|
<div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label data-i18n="GLOBAL/FROM"></label>
|
||||||
|
<input type="text" autofocus="" autocomplete="off" autocorrect="off" autocapitalize="off"
|
||||||
|
data-bind="value: from">
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label data-i18n="GLOBAL/TO"></label>
|
||||||
|
<input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" data-bind="value: to">
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label data-i18n="GLOBAL/SUBJECT"></label>
|
||||||
|
<input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" data-bind="value: subject">
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label data-i18n="SEARCH/TEXT"></label>
|
||||||
|
<input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" data-bind="value: text">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label data-i18n="SEARCH/DATE"></label>
|
||||||
|
<div data-bind="component: {
|
||||||
|
name: 'Select',
|
||||||
|
params: {
|
||||||
|
options: selectedDates,
|
||||||
|
value: selectedDateValue,
|
||||||
|
optionsText: 'name',
|
||||||
|
optionsValue: 'id'
|
||||||
|
}
|
||||||
|
}"></div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label data-i18n="SEARCH/REPLIED"></label>
|
||||||
|
<div data-bind="component: {
|
||||||
|
name: 'Select',
|
||||||
|
params: {
|
||||||
|
options: repliedOptions,
|
||||||
|
value: repliedValue,
|
||||||
|
optionsText: 'name',
|
||||||
|
optionsValue: 'id'
|
||||||
|
}
|
||||||
|
}"></div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label></label>
|
||||||
|
<div>
|
||||||
|
<div data-bind="component: {
|
||||||
|
name: 'Checkbox',
|
||||||
|
params: {
|
||||||
|
label: 'SEARCH/UNSEEN',
|
||||||
|
value: unseen
|
||||||
|
}
|
||||||
|
}"></div>
|
||||||
|
<div data-bind="component: {
|
||||||
|
name: 'Checkbox',
|
||||||
|
params: {
|
||||||
|
label: 'SEARCH/FLAGGED',
|
||||||
|
value: starred
|
||||||
|
}
|
||||||
|
}"></div>
|
||||||
|
<div data-bind="component: {
|
||||||
|
name: 'Checkbox',
|
||||||
|
params: {
|
||||||
|
label: 'SEARCH/HAS_ATTACHMENT',
|
||||||
|
value: hasAttachment
|
||||||
|
}
|
||||||
|
}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<footer style="display: flex; justify-content: space-between;">
|
||||||
|
<button class="btn" data-i18n="SFILTERS/EDIT_FILTERS" data-bind="click: editFilters"></button>
|
||||||
|
<button form="advsearchform" class="btn" data-icon="✔" data-i18n="SFILTERS/SAVE"></button>
|
||||||
|
</footer>
|
||||||
54
plugins/search-filters/templates/PopupsSearchFilters.html
Normal file
54
plugins/search-filters/templates/PopupsSearchFilters.html
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<header>
|
||||||
|
<a href="#" class="close" data-bind="click: close">×</a>
|
||||||
|
<h3 data-i18n="SFILTERS/WHEN_MESSAGE_MATCH_SEARCH_CRITERIA"></h3>
|
||||||
|
</header>
|
||||||
|
<form id="search-filters-form" class="modal-body form-horizontal" action="#/" autocomplete="off" spellcheck="false"
|
||||||
|
data-bind="submit: submitForm">
|
||||||
|
<div>
|
||||||
|
<div class="control-group">
|
||||||
|
<div data-bind="component: {
|
||||||
|
name: 'Checkbox',
|
||||||
|
params: {
|
||||||
|
label: 'SFILTERS/MOVE_TO_FOLDER',
|
||||||
|
value: usefFolder
|
||||||
|
}
|
||||||
|
}"></div>
|
||||||
|
<select data-bind="
|
||||||
|
options: folders,
|
||||||
|
value: fFolder,
|
||||||
|
optionsText: 'name',
|
||||||
|
optionsValue: 'name',
|
||||||
|
enable: usefFolder
|
||||||
|
"></select>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<div data-bind="component: {
|
||||||
|
name: 'Checkbox',
|
||||||
|
params: {
|
||||||
|
label: 'SFILTERS/MARK_AS_READ',
|
||||||
|
value: fSeen
|
||||||
|
}
|
||||||
|
}"></div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<div data-bind="component: {
|
||||||
|
name: 'Checkbox',
|
||||||
|
params: {
|
||||||
|
label: 'SFILTERS/STAR_IT',
|
||||||
|
value: fFlag
|
||||||
|
}
|
||||||
|
}"></div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="priority" data-i18n="SFILTERS/PRIORITY_LABLE"></label>
|
||||||
|
<select data-bind="
|
||||||
|
options: Priorities,
|
||||||
|
value: priority,
|
||||||
|
"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<footer>
|
||||||
|
<button form="search-filters-form" class="btn buttonSearchFilters" data-icon="✔"
|
||||||
|
data-i18n="SFILTERS/CREATE_FILTER"></button>
|
||||||
|
</footer>
|
||||||
39
plugins/search-filters/templates/STabSearchFilters.html
Normal file
39
plugins/search-filters/templates/STabSearchFilters.html
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<div>
|
||||||
|
<div class="form-horizontal">
|
||||||
|
<div class="legend">
|
||||||
|
<span data-i18n="SFILTERS/THE_FOLLOWING_FILTERS_ARE_APPLIED"></span>
|
||||||
|
|
||||||
|
<i class="icon-spinner animated" style="margin-top: 5px" data-bind="visible: loading"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-bind="visible: ioFilters().length == 0">
|
||||||
|
<span data-i18n="SFILTERS/NO_FILTERS"> </span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ko foreach: ioFilters -->
|
||||||
|
<label class="control-label">
|
||||||
|
<span data-bind="text: $root.i18n('MATCHES')"> </span>
|
||||||
|
<span data-bind="text: searchQ"></span>
|
||||||
|
|
||||||
|
<span data-bind="text: $root.i18n('PRIORITY_POINTS')"></span>
|
||||||
|
:
|
||||||
|
|
||||||
|
<span data-bind="text: priority"></span>
|
||||||
|
<br>
|
||||||
|
<span data-bind="text: $root.i18n('DO_THIS')"></span>
|
||||||
|
<span data-bind="visible: fFolder, text: $root.i18n('MOVE_TO_FOLDER')"></span>
|
||||||
|
<span data-bind="visible: fFolder, text: fFolder"></span>
|
||||||
|
|
||||||
|
<span data-bind="visible: fSeen, text: $root.i18n('MARK_AS_READ')"></span>
|
||||||
|
|
||||||
|
<span data-bind="visible: fFlag, text: $root.i18n('STAR_IT')"></span>
|
||||||
|
</label>
|
||||||
|
<div class="controls">
|
||||||
|
<button class="btn" data-bind="click: $root.edit, text: $root.i18n('EDIT')"></button>
|
||||||
|
<button class="btn" data-bind="click: $root.remove, text: $root.i18n('REMOVE')"></button>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<!-- /ko -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue