mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-09-20 07:35:55 +08:00
Add Search Filters Plugin with Gmail-like Functionality
Introducing a new plugin for enhanced email organization: Search Filters, inspired by similar features in Gmail (Search Filters) and Outlook (Rules). The plugin allows you to define custom search queries that automatically perform one or more actions on matching emails based on their priority level (1-10). Available actions include marking as seen, flagging/starring, and moving to a designated folder. This plugin is designed to work seamlessly with both old and new emails in your inbox, taking into account the specified search query criteria. However, please be aware of an outstanding bug that affects the display of unread email counts beside folders; this issue has yet to be resolved. I have developed this plugin as a freelance project for a client on their custom server setup. Unfortunately, I never received any feedback or response from my client after completion of the work. Nonetheless, I hope that sharing it here will benefit others and contribute to the ongoing advancement of this open-source project. Please note: This plugin is offered as-is with no warranties or guarantees. Make sure to thoroughly test and verify its functionality before using it in a production environment. Thank you to everyone who has contributed to making this project a reality.
This commit is contained in:
parent
e4316e6665
commit
4a56d2d087
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…
Reference in a new issue