diff --git a/src/content/mytinytodo.js b/src/content/mytinytodo.js index bc1b958..50c7b96 100644 --- a/src/content/mytinytodo.js +++ b/src/content/mytinytodo.js @@ -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 '' + escapeHtml(tag) + ''; + return `${escapeHtml(tag)}`; } }, @@ -1988,11 +1998,14 @@ function loadTags(listId, callback) let cloud = ''; tagsList.forEach( item => { // item.tag is escaped with htmlspecialchars() - cloud += ' ' + item.tag + ''; + cloud += ` ${item.tag}`; }); if (cloud == '') { cloud = _mtt.lang.get('noTags'); } + else { + cloud = `${_mtt.lang.get('withoutTags')}` + 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 += `
  • ${list.name}
  • `; }) diff --git a/src/includes/api/TasksController.php b/src/includes/api/TasksController.php index 0a5bc14..1a1a5f1 100644 --- a/src/includes/api/TasksController.php +++ b/src/includes/api/TasksController.php @@ -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 "); diff --git a/src/includes/class.lang.php b/src/includes/class.lang.php index f43fc2e..d86524b 100644 --- a/src/includes/class.lang.php +++ b/src/includes/class.lang.php @@ -127,6 +127,8 @@ class Lang 'listNotFound', 'noPublicLists', 'noTags', + 'withoutTags', + 'withAnyTag', 'invalidpass', 'addList', 'addListDefault', diff --git a/src/includes/lang/en.json b/src/includes/lang/en.json index 9f84948..57c1435 100644 --- a/src/includes/lang/en.json +++ b/src/includes/lang/en.json @@ -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", diff --git a/src/includes/lang/ru.json b/src/includes/lang/ru.json index 11bd3cd..17eaa28 100644 --- a/src/includes/lang/ru.json +++ b/src/includes/lang/ru.json @@ -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",