+ Can filter tasks by having any tag or not having tags (closes GH-64)

This commit is contained in:
maxpozdeev 2025-02-17 12:30:30 +03:00
parent 4548aa17de
commit 8082278af5
5 changed files with 65 additions and 25 deletions

View file

@ -312,7 +312,8 @@ var mytinytodo = window.mytinytodo = _mtt = {
$('#tagcloudcontent').on('click', '.tag', function(event){
//tag is not escaped
addFilterTag( this.dataset.tag, this.dataset.tagId, (event.metaKey || event.ctrlKey ? true : false) );
if(_mtt.menus.tagcloud) _mtt.menus.tagcloud.close();
if (_mtt.menus.tagcloud)
_mtt.menus.tagcloud.close();
return false;
});
@ -948,25 +949,32 @@ var mytinytodo = window.mytinytodo = _mtt = {
filter: {
_filters: [],
clear: function() {
clear() {
this._filters = [];
$('#mtt-tag-toolbar').hide();
$('#mtt-tag-filters').html('');
},
addTag: function(tagId, tag, exclude)
addTag(tagId, tag, exclude)
{
tagId += 0;
for (let i in this._filters) {
if (this._filters[i].tagId && this._filters[i].tagId == tagId)
for (const filter of this._filters) {
if (filter.tagId && filter.tagId == tagId)
return false;
}
this._filters.push({tagId:tagId, tag:tag, exclude:exclude});
if (tagId == -1) {
// for display purposes only
tag = exclude ? _mtt.lang.get('withAnyTag') : _mtt.lang.get('withoutTags');
exclude = false;
}
const tagHtml = this.prepareTagHtml(tagId, tag, ['tag-filter', 'tag-id-'+tagId, exclude ? 'tag-filter-exclude' : '']) ;
$('#mtt-tag-filters').append(tagHtml);
$('#mtt-tag-toolbar').show();
return true;
},
cancelTag: function(tagId)
cancelTag(tagId)
{
for (let i in this._filters) {
if (this._filters[i].tagId && this._filters[i].tagId == tagId) {
@ -980,23 +988,25 @@ var mytinytodo = window.mytinytodo = _mtt = {
}
return false;
},
getTags: function(withExcluded)
getTags(withExcluded)
{
let a = [];
for (let i in this._filters) {
if (this._filters[i].tagId) {
if (this._filters[i].exclude && withExcluded)
a.push('^'+ this._filters[i].tag);
else if (!this._filters[i].exclude)
a.push(this._filters[i].tag)
for (const filter of this._filters) {
if (filter.tagId) {
if (filter.exclude && withExcluded)
a.push('^'+ filter.tag);
else if (!filter.exclude)
a.push(filter.tag)
}
}
return a.join(', ');
},
prepareTagHtml: function(tagId, tag, classes)
prepareTagHtml(tagId, tag, classes)
{
// tag is not escaped
return '<span class="' + classes.join(' ') + ' mtt-filter-close" tagid="' + tagId + '">' + escapeHtml(tag) + '<span class="tag-filter-btn"></span></span>';
return `<span class="${classes.join(' ')} mtt-filter-close" tagid="${tagId}">${escapeHtml(tag)}<span class="tag-filter-btn"></span></span>`;
}
},
@ -1988,11 +1998,14 @@ function loadTags(listId, callback)
let cloud = '';
tagsList.forEach( item => {
// item.tag is escaped with htmlspecialchars()
cloud += ' <span class="tag" data-tag="' + item.tag + '" data-tag-id="' + item.id + '">' + item.tag + '</span>';
cloud += ` <span class="tag" data-tag="${item.tag}" data-tag-id="${item.id}">${item.tag}</span>`;
});
if (cloud == '') {
cloud = _mtt.lang.get('noTags');
}
else {
cloud = `<span class="tag" data-tag="^" data-tag-id="-1">${_mtt.lang.get('withoutTags')}</span>` + cloud;
}
$('#tagcloudcontent').html(cloud)
flag.tagsChanged = false;
callback();
@ -2008,7 +2021,8 @@ function cancelTagFilter(tagId, dontLoadTasks)
function addFilterTag(tag, tagId, exclude)
{
if(!_mtt.filter.addTag(tagId, tag, exclude)) return false;
if (!_mtt.filter.addTag(tagId, tag, exclude))
return false;
loadTasks();
};
@ -2621,7 +2635,7 @@ function slmenuOnListsLoaded()
let s = '';
tabLists.getAll().forEach( (list) => {
const classChecked = (list.id == curList.id) ? 'mtt-item-checked' : '';
const classHidden = list.hidden ? 'mtt-list-hidden' : '';
const classHidden = list.hidden ? 'mtt-list-hidden' : '';
s += `<li id="slmenu_list:${list.id}" class="list-id-${list.id} ${classChecked} ${classHidden}">
<div class="menu-icon"></div><a href="${_mtt.urlForList(list)}">${list.name}</a><div class="counter hidden"></div></li>`;
})

View file

@ -24,7 +24,7 @@ class TasksController extends ApiController {
$db = DBConnection::instance();
$dbcore = DBCore::default();
$sqlWhere = $sqlWhereListId = '';
$sqlWhere = $sqlWhereListId = $sqlHaving = '';
$userLists = [];
if ($listId == -1) {
$userLists = $this->getUserListsSimple();
@ -46,8 +46,26 @@ class TasksController extends ApiController {
$tagExIds = array();
foreach ($at as $atv) {
$atv = trim($atv);
if ($atv == '' || $atv == '^') continue;
if (substr($atv,0,1) == '^') {
if ($atv == '')
continue;
// tasks without tags (ignore other tags included or excluded)
if ($atv == '^') {
$tagIds = [];
$tagExIds = [];
if ($db::DBTYPE == DBConnection::DBTYPE_MYSQL)
$sqlHaving = "tags_ids = ''";
else
$sqlHaving = "string_agg(tags.name, ',') IS NULL"; // catches if tag name is ''
break;
}
// tasks with any tag
else if ($atv == '^^') {
if ($db::DBTYPE == DBConnection::DBTYPE_MYSQL)
$sqlHaving = "tags_ids != ''";
else
$sqlHaving = "string_agg(tags.name, ',') != ''";
}
else if (substr($atv,0,1) == '^') {
array_push($tagExIds, ...$dbcore->getTagIdsByName(substr($atv,1)));
} else {
$tagIds[] = $dbcore->getTagIdsByName($atv);
@ -110,6 +128,8 @@ class TasksController extends ApiController {
else {
$groupConcat = "GROUP_CONCAT(tags.id) AS tags_ids, GROUP_CONCAT(tags.name) AS tags";
}
if ($sqlHaving != '')
$sqlHaving = "HAVING $sqlHaving";
$q = $db->dq("
SELECT todo.*, todo.duedate IS NULL AS ddn, $groupConcat
@ -117,7 +137,7 @@ class TasksController extends ApiController {
LEFT JOIN {$db->prefix}tag2task AS t2t ON todo.id = t2t.task_id
LEFT JOIN {$db->prefix}tags AS tags ON t2t.tag_id = tags.id
WHERE $sqlWhereListId $sqlWhere
GROUP BY todo.id
GROUP BY todo.id $sqlHaving
$sqlSort
");

View file

@ -127,6 +127,8 @@ class Lang
'listNotFound',
'noPublicLists',
'noTags',
'withoutTags',
'withAnyTag',
'invalidpass',
'addList',
'addListDefault',

View file

@ -1,7 +1,7 @@
{
"_header": {
"ver": "v1.8.2",
"date": "2025-02-15",
"date": "2025-02-16",
"language": "English",
"original_name": "English",
"authors": [
@ -205,6 +205,8 @@
"listNotFound": "List not found",
"noPublicLists": "No public tasks",
"noTags": "No tags",
"withoutTags": "No tags",
"withAnyTag": "Any tag",
"invalidpass": "Wrong password",
"addList": "Create new list",
"addListDefault": "Todo",

View file

@ -1,7 +1,7 @@
{
"_header": {
"ver": "v1.8.2",
"date": "2025-02-15",
"date": "2025-02-16",
"language": "Russian",
"original_name": "Русский",
"authors": [
@ -205,6 +205,8 @@
"listNotFound": "Список не найден",
"noPublicLists": "Нет опубликованных списков",
"noTags": "Нет тегов",
"withoutTags": "Без тегов",
"withAnyTag": "Любой тег",
"invalidpass": "Неверный пароль",
"addList": "Новый список",
"addListDefault": "Todo",