- {% for item in submenu.sorted_items %}
+ {% for item in menu_items %}
{% if not item.is_enabled %}
{% if advanced_mode or not item.advanced %}
{% include "card.html" %}
diff --git a/plinth/tests/tags/__init__.py b/plinth/tests/tags/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/plinth/tests/tags/test_functional.py b/plinth/tests/tags/test_functional.py
new file mode 100644
index 000000000..85c8bd6f1
--- /dev/null
+++ b/plinth/tests/tags/test_functional.py
@@ -0,0 +1,60 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+Functional, browser based tests for transmission app.
+"""
+
+import pytest
+
+from selenium.webdriver.common.keys import Keys
+from plinth.tests import functional
+
+pytestmark = [pytest.mark.tags]
+
+
+def _is_app_listed(session_browser, app):
+ """Assert that the specified app is listed on the page."""
+ app_links = session_browser.links.find_by_href(f'/plinth/apps/{app}/')
+ assert len(app_links) == 1
+
+
+@pytest.fixture(autouse=True)
+def fixture_bittorrent_tag(session_browser):
+ """Click on the BitTorrent tag."""
+ bittorrent_tag = '/plinth/apps/?tag=BitTorrent'
+ functional.login(session_browser)
+ functional.nav_to_module(session_browser, 'transmission')
+ with functional.wait_for_page_update(session_browser, timeout=10,
+ expected_url=bittorrent_tag):
+ session_browser.links.find_by_href(bittorrent_tag).click()
+
+
+def test_bittorrent_tag(session_browser):
+ """Test that the BitTorrent tag lists Deluge and Transmission."""
+ _is_app_listed(session_browser, 'deluge')
+ _is_app_listed(session_browser, 'transmission')
+
+
+def test_search_for_tag(session_browser):
+ """Test that searching for a tag returns the expected apps."""
+ search_input = session_browser.driver.find_element_by_id('add-tag-input')
+ with functional.wait_for_page_update(
+ session_browser, timeout=10,
+ expected_url='/plinth/apps/?tag=BitTorrent&tag=File%20Sharing'):
+ search_input.click()
+ search_input.send_keys('file')
+ search_input.send_keys(Keys.ENTER)
+ for app in ['deluge', 'nextcloud', 'sharing', 'syncthing', 'transmission']:
+ _is_app_listed(session_browser, app)
+
+
+def test_click_on_tag(session_browser):
+ """Test that clicking on a tag lists the expected apps."""
+ search_input = session_browser.driver.find_element_by_id('add-tag-input')
+ with functional.wait_for_page_update(
+ session_browser, timeout=10,
+ expected_url='/plinth/apps/?tag=BitTorrent&tag=Cloud%20Storage'):
+ search_input.click()
+ session_browser.find_by_css(
+ ".dropdown-item[data-value='Cloud Storage']").click()
+ for app in ['deluge', 'nextcloud', 'syncthing', 'transmission']:
+ _is_app_listed(session_browser, app)
diff --git a/plinth/views.py b/plinth/views.py
index be07617a4..ddd4eb312 100644
--- a/plinth/views.py
+++ b/plinth/views.py
@@ -23,6 +23,7 @@ from django.views.generic.edit import FormView
from stronghold.decorators import public
from plinth import app as app_module
+from plinth import menu
from plinth.daemon import app_is_running
from plinth.modules.config import get_advanced_mode
from plinth.modules.firewall.components import get_port_forwarding_info
@@ -120,13 +121,61 @@ def index(request):
class AppsIndexView(TemplateView):
- """View for apps index"""
+ """View for apps index.
+
+ This view supports filtering apps by one or more tags. If no tags are
+ provided, it will show all the apps. If one or more tags are provided,
+ it will select apps matching any of the provided tags.
+ """
template_name = 'apps.html'
+ @staticmethod
+ def _pick_menu_items(menu_items, selected_tags):
+ """Return a sorted list of menu items filtered by tags."""
+
+ def _mismatch_map(menu_item) -> list[bool]:
+ """Return a list of mismatches for selected tags.
+
+ A mismatch is when a selected tag is *not* present in the list of
+ tags for menu item.
+ """
+ menu_tags = set(menu_item.app.info.tags)
+ return [tag not in menu_tags for tag in selected_tags]
+
+ def _sort_key(menu_item):
+ """Returns a comparable tuple to sort menu items.
+
+ Sort items by tag match count first, then by the order of matched
+ tags in user specified order, then by the order set by menu item,
+ and then by the name of the menu item in current locale (by
+ configured collation order).
+ """
+ return (_mismatch_map(menu_item).count(True),
+ _mismatch_map(menu_item), menu_item.order,
+ menu_item.name.lower())
+
+ # Filter out menu items that don't match any of the selected tags. If
+ # no tags are selected, return all menu items. Otherwise, return all
+ # menu items that have at least one matching tag.
+ filtered_menu_items = [
+ menu_item for menu_item in menu_items
+ if (not selected_tags) or (not all(_mismatch_map(menu_item)))
+ ]
+
+ return sorted(filtered_menu_items, key=_sort_key)
+
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['show_disabled'] = True
context['advanced_mode'] = get_advanced_mode()
+
+ tags = self.request.GET.getlist('tag', [])
+ menu_items = menu.main_menu.active_item(self.request).items
+
+ context['tags'] = tags
+ context['all_tags'] = app_module.Info.list_tags()
+ context['menu_items'] = self._pick_menu_items(menu_items, tags)
+
return context
@@ -364,8 +413,9 @@ class SetupView(TemplateView):
context['setup_state'] = setup_state
context['operations'] = operation.manager.filter(app.app_id)
context['show_rerun_setup'] = False
- context['show_uninstall'] = (not app.info.is_essential and setup_state
- != app_module.App.SetupState.NEEDS_SETUP)
+ context['show_uninstall'] = (
+ not app.info.is_essential
+ and setup_state != app_module.App.SetupState.NEEDS_SETUP)
# Perform expensive operation only if needed.
if not context['operations']:
diff --git a/static/tags.js b/static/tags.js
new file mode 100644
index 000000000..822ec9b06
--- /dev/null
+++ b/static/tags.js
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+/**
+ * @licstart The following is the entire license notice for the JavaScript
+ * code in this page.
+ *
+ * This file is part of FreedomBox.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see
.
+ *
+ * @licend The above is the entire license notice for the JavaScript code
+ * in this page.
+ */
+
+/**
+ * Update the URL path and history based on the selected tags.
+ *
+ * If no tags are selected, redirects to the base apps path. Otherwise,
+ * constructs a new URL with query parameters for each tag and updates
+ * the browser history.
+ *
+ * @param {string[]} tags - An array of selected tag names.
+ */
+function updatePath(tags) {
+ const appsPath = window.location.pathname;
+ if (tags.length === 0) {
+ this.location.assign(appsPath);
+ } else {
+ let queryParams = tags.map(tag => `tag=${tag}`).join('&');
+ let newPath = `${appsPath}?${queryParams}`;
+ this.history.pushState({ tags: tags }, '', newPath);
+ this.location.assign(newPath);
+ }
+}
+
+/**
+ * Get a list of tags currently displayed, excluding a specific tag if provided.
+ *
+ * Iterates through the tag badges in the UI, extracts their text content,
+ * and returns an array of tag names.
+ *
+ * @param {string} [tagToRemove] - The name of the tag to exclude.
+ * @returns {string[]} An array of tag names currently displayed.
+ */
+function getTags(tagToRemove) {
+ const tagBadges = document.querySelectorAll('#selected-tags .tag-badge');
+ return Array.from(tagBadges)
+ .map(tag => tag.dataset.tag)
+ .filter(tagName => tagName !== tagToRemove);
+}
+
+/**
+ * Filter and highlight the best matching tag based on the search term.
+ *
+ * This function updates the visibility and highlighting of dropdown items
+ * to match the user's input in the search box. It determines the best
+ * matching item and marks it as active.
+ *
+ * @param {KeyboardEvent} event - The keyboard event that triggered the search.
+ */
+function findMatchingTag(addTagInput, dropdownItems) {
+ const searchTerm = addTagInput.value.toLowerCase().trim();
+
+ // Remove highlighting from all items
+ dropdownItems.forEach(item => item.classList.remove('active'));
+
+ let bestMatch = null;
+ dropdownItems.forEach(item => {
+ const text = item.textContent.toLowerCase();
+ if (text.includes(searchTerm)) {
+ item.style.display = 'block';
+ function matchesEarly () {
+ return text.indexOf(searchTerm) < bestMatch.textContent.toLowerCase().indexOf(searchTerm);
+ };
+ if (bestMatch === null || matchesEarly()) {
+ bestMatch = item;
+ }
+ } else {
+ item.style.display = 'none';
+ }
+ });
+
+ // Highlight the best match
+ if (bestMatch) {
+ bestMatch.classList.add('active');
+ }
+}
+
+/**
+ * Manage tag-related UI interactions for filtering and displaying apps.
+ *
+ * This script manages the user interface for filtering apps based on
+ * selected tags. It provides functionality for adding and removing tags,
+ * updating the URL based on selected tags, and displaying a set of
+ * available tags in a searchable dropdown.
+ */
+document.addEventListener('DOMContentLoaded', function () {
+
+ // Remove Tag handler.
+ document.querySelectorAll('.remove-tag').forEach(button => {
+ button.addEventListener('click', () => {
+ let tag = button.dataset.tag;
+ let tags = getTags(tag);
+ updatePath(tags);
+ });
+ });
+
+ /**
+ * Searchable dropdown for selecting tags.
+ *
+ * As the user types in the input field, the dropdown list is filtered
+ * to show only matching items. The best matching item (first match if
+ * multiple match) is highlighted. Pressing Enter selects the
+ * highlighted item and adds it as a tag.
+ */
+ const addTagInput = document.getElementById('add-tag-input');
+ const dropdownItems = document.querySelectorAll('li.dropdown-item');
+
+ var timeoutId;
+ addTagInput.addEventListener('keyup', (event) => {
+ clearTimeout(timeoutId);
+ // Select the active tag if the user pressed Enter
+ if (event.key === 'Enter') {
+ dropdownItems.forEach(item => {
+ if (item.classList.contains('active')) {
+ item.click();
+ }
+ });
+ }
+ // Debounce the user input for search with 300ms delay.
+ timeoutId = setTimeout(findMatchingTag(addTagInput, dropdownItems), 300);
+ });
+
+ dropdownItems.forEach(item => {
+ item.addEventListener('click', () => {
+ const selectedTag = item.dataset.value;
+
+ // Add the selected tag and update the path.
+ let tags = getTags('');
+ tags.push(selectedTag);
+ updatePath(tags);
+
+ // Reset the input field and dropdown.
+ addTagInput.value = '';
+ dropdownItems.forEach(item => {
+ item.style.display = '';
+ item.classList.remove('active');
+ });
+ });
+ });
+
+ // Handle browser back/forward navigation.
+ window.addEventListener('popstate', function (event) {
+ if (event.state && event.state.tags) {
+ updatePath(event.state.tags);
+ }
+ });
+});
+
diff --git a/static/themes/default/css/main.css b/static/themes/default/css/main.css
index f2ff58061..bc636d527 100644
--- a/static/themes/default/css/main.css
+++ b/static/themes/default/css/main.css
@@ -309,7 +309,6 @@ html {
}
#wrapper {
- min-height: 100%;
position: relative;
}
@@ -321,6 +320,59 @@ html {
margin-bottom: 1.25rem;
}
+/* Tag Input Container */
+.tag-input {
+ display: flex;
+ align-items: center;
+ border: 1px solid #ced4da;
+ border-radius: .25rem;
+ padding: .375rem .75rem;
+ width: 100%;
+ background-color: #fff;
+ margin-bottom: 3rem;
+}
+
+/* Selected Tags */
+.tag-input #selected-tags {
+ display: flex;
+ flex-wrap: wrap;
+ margin-right: .5rem;
+}
+
+.tag-input .tag {
+ background-color: #e9ecef; /* Light gray background */
+ border-radius: .25rem;
+ padding: .25rem .5rem;
+ margin-right: .25rem;
+ margin-bottom: .25rem;
+ display: flex;
+ align-items: center;
+}
+
+/* Remove tag button */
+.tag-badge .remove-tag {
+ background-color: #f8f9fa; /* Match the tag's background color */
+ border: none;
+ padding: 0.25rem 0.5rem;
+ cursor: pointer;
+}
+
+/* Adjust input field width */
+.tag-input input[type="search"] {
+ flex-grow: 1;
+ border: none;
+ outline: none;
+ box-shadow: none;
+ width: auto;
+ min-width: 3rem;
+}
+
+/* dropdown-menu for tags is a scrollable list */
+.tag-input .dropdown-menu {
+ bottom: calc(-100% - 10px);
+ overflow-y: auto;
+}
+
@media (min-width: 768px) {
.content-container {
padding: 1.5rem 3rem 3rem;
From 5fa9bf292893188341214cb71e06661dec082d2f Mon Sep 17 00:00:00 2001
From: Sunil Mohan Adapa
Date: Tue, 15 Oct 2024 15:25:28 -0700
Subject: [PATCH 35/56] *: tags: Adjust tags and style
- Don't use title casing, instead use simple capitalization.
- Add some tags.
- Drop outdated tags like 'VoIP', 'IM' while emphasizing 'Audio chat', 'Video
chat', 'Encrypted messaging' instead.
- Try to clarify server vs. web client with tags.
Signed-off-by: Sunil Mohan Adapa
---
plinth/modules/bepasty/manifest.py | 2 +-
plinth/modules/calibre/manifest.py | 2 +-
plinth/modules/coturn/manifest.py | 2 +-
plinth/modules/deluge/manifest.py | 2 +-
plinth/modules/ejabberd/manifest.py | 8 +++-----
plinth/modules/email/manifest.py | 2 +-
plinth/modules/featherwiki/manifest.py | 2 +-
plinth/modules/gitweb/manifest.py | 2 +-
plinth/modules/i2p/manifest.py | 2 +-
plinth/modules/infinoted/manifest.py | 2 +-
plinth/modules/janus/manifest.py | 2 +-
plinth/modules/jsxc/manifest.py | 2 +-
plinth/modules/kiwix/manifest.py | 7 ++++++-
plinth/modules/matrixsynapse/manifest.py | 12 +++++-------
plinth/modules/minetest/manifest.py | 2 +-
plinth/modules/minidlna/manifest.py | 2 +-
plinth/modules/miniflux/manifest.py | 2 +-
plinth/modules/mumble/manifest.py | 2 +-
plinth/modules/nextcloud/manifest.py | 2 +-
plinth/modules/openvpn/manifest.py | 2 +-
plinth/modules/privoxy/manifest.py | 2 +-
plinth/modules/quassel/manifest.py | 2 +-
plinth/modules/radicale/manifest.py | 8 +-------
plinth/modules/roundcube/manifest.py | 3 ++-
plinth/modules/rssbridge/manifest.py | 6 ++----
plinth/modules/samba/manifest.py | 8 +++++++-
plinth/modules/searx/manifest.py | 2 +-
plinth/modules/shaarli/manifest.py | 2 +-
plinth/modules/shadowsocks/manifest.py | 10 +++++-----
plinth/modules/shadowsocksserver/manifest.py | 7 ++++++-
plinth/modules/sharing/manifest.py | 2 +-
plinth/modules/syncthing/manifest.py | 2 +-
plinth/modules/tiddlywiki/manifest.py | 6 +++---
plinth/modules/tor/manifest.py | 7 ++++---
plinth/modules/torproxy/manifest.py | 7 +++----
plinth/modules/transmission/manifest.py | 2 +-
plinth/modules/ttrss/manifest.py | 2 +-
plinth/modules/wireguard/manifest.py | 2 +-
plinth/modules/wordpress/manifest.py | 2 +-
plinth/modules/zoph/manifest.py | 2 +-
plinth/tests/tags/test_functional.py | 10 +++++-----
41 files changed, 80 insertions(+), 75 deletions(-)
diff --git a/plinth/modules/bepasty/manifest.py b/plinth/modules/bepasty/manifest.py
index 6eb552dbb..754532584 100644
--- a/plinth/modules/bepasty/manifest.py
+++ b/plinth/modules/bepasty/manifest.py
@@ -20,4 +20,4 @@ backup = {
'services': ['uwsgi'],
}
-tags = [_('File Sharing'), _('Pastebin')]
+tags = [_('File sharing'), _('Pastebin')]
diff --git a/plinth/modules/calibre/manifest.py b/plinth/modules/calibre/manifest.py
index f92b136ed..005fcc64a 100644
--- a/plinth/modules/calibre/manifest.py
+++ b/plinth/modules/calibre/manifest.py
@@ -17,4 +17,4 @@ backup = {
'services': ['calibre-server-freedombox']
}
-tags = [_('Ebook'), _('Library'), _('Ebook Reader')]
+tags = [_('Ebook'), _('Library'), _('Ebook reader')]
diff --git a/plinth/modules/coturn/manifest.py b/plinth/modules/coturn/manifest.py
index 777f09a21..779da3716 100644
--- a/plinth/modules/coturn/manifest.py
+++ b/plinth/modules/coturn/manifest.py
@@ -4,4 +4,4 @@ from django.utils.translation import gettext_lazy as _
backup = {'secrets': {'directories': ['/etc/coturn']}, 'services': ['coturn']}
-tags = [_('VoIP'), _('STUN'), _('TURN')]
+tags = [_('Video conference'), _('STUN'), _('TURN')]
diff --git a/plinth/modules/deluge/manifest.py b/plinth/modules/deluge/manifest.py
index 7335ea1b5..c3d4b0bbf 100644
--- a/plinth/modules/deluge/manifest.py
+++ b/plinth/modules/deluge/manifest.py
@@ -18,4 +18,4 @@ backup = {
'services': ['deluged', 'deluge-web']
}
-tags = [_('File Sharing'), _('BitTorrent'), _('Client'), _('P2P')]
+tags = [_('File sharing'), _('BitTorrent'), _('Web client'), _('P2P')]
diff --git a/plinth/modules/ejabberd/manifest.py b/plinth/modules/ejabberd/manifest.py
index 62108c4ce..825ac2e6c 100644
--- a/plinth/modules/ejabberd/manifest.py
+++ b/plinth/modules/ejabberd/manifest.py
@@ -121,10 +121,8 @@ backup = {
}
tags = [
+ _('Encrypted messaging'),
+ _('Audio chat'),
+ _('Video chat'),
_('XMPP'),
- _('VoIP'),
- _('IM'),
- _('Encrypted Messaging'),
- _('Audio Chat'),
- _('Video Chat')
]
diff --git a/plinth/modules/email/manifest.py b/plinth/modules/email/manifest.py
index 6097000e2..8927516fb 100644
--- a/plinth/modules/email/manifest.py
+++ b/plinth/modules/email/manifest.py
@@ -79,4 +79,4 @@ backup = {
'services': ['postfix', 'dovecot', 'rspamd']
}
-tags = [_('Email')]
+tags = [_('Email server'), _('IMAP'), _('Spam control')]
diff --git a/plinth/modules/featherwiki/manifest.py b/plinth/modules/featherwiki/manifest.py
index 2855ee098..6e6d569fd 100644
--- a/plinth/modules/featherwiki/manifest.py
+++ b/plinth/modules/featherwiki/manifest.py
@@ -15,4 +15,4 @@ clients = [{
backup = {'data': {'directories': [str(wiki_dir)]}}
-tags = [_('Wiki'), _('Note Taking'), _('Website'), _('Quine'), _('non-Debian')]
+tags = [_('Wiki'), _('Note taking'), _('Website'), _('Quine'), _('Non-Debian')]
diff --git a/plinth/modules/gitweb/manifest.py b/plinth/modules/gitweb/manifest.py
index f48d69410..c5102c31e 100644
--- a/plinth/modules/gitweb/manifest.py
+++ b/plinth/modules/gitweb/manifest.py
@@ -34,4 +34,4 @@ clients = [
backup = {'data': {'directories': [GIT_REPO_PATH]}}
-tags = [_('Git'), _('Version Control'), _('Dev Tool')]
+tags = [_('Git hosting'), _('Version control'), _('Developer tool')]
diff --git a/plinth/modules/i2p/manifest.py b/plinth/modules/i2p/manifest.py
index 2f1f64beb..8e4cdda96 100644
--- a/plinth/modules/i2p/manifest.py
+++ b/plinth/modules/i2p/manifest.py
@@ -40,4 +40,4 @@ backup = {
'services': ['i2p']
}
-tags = [_('Anonymity Network'), _('Censorship Resistance')]
+tags = [_('Anonymity network'), _('Censorship resistance')]
diff --git a/plinth/modules/infinoted/manifest.py b/plinth/modules/infinoted/manifest.py
index fad1f5786..cc27ca983 100644
--- a/plinth/modules/infinoted/manifest.py
+++ b/plinth/modules/infinoted/manifest.py
@@ -43,4 +43,4 @@ backup = {
'services': ['infinoted']
}
-tags = [_('Note Taking'), _('Collaborative Editing'), _('Gobby')]
+tags = [_('Note taking'), _('Collaborative editing'), _('Gobby')]
diff --git a/plinth/modules/janus/manifest.py b/plinth/modules/janus/manifest.py
index 780f75efb..e8ece3120 100644
--- a/plinth/modules/janus/manifest.py
+++ b/plinth/modules/janus/manifest.py
@@ -13,4 +13,4 @@ clients = [{
backup: dict = {}
-tags = [_('Video Conferencing'), _('WebRTC')]
+tags = [_('Video conference'), _('WebRTC'), _('Web conference')]
diff --git a/plinth/modules/jsxc/manifest.py b/plinth/modules/jsxc/manifest.py
index 565924b3f..00dffa0c4 100644
--- a/plinth/modules/jsxc/manifest.py
+++ b/plinth/modules/jsxc/manifest.py
@@ -13,4 +13,4 @@ clients = [{
backup: dict = {}
-tags = [_('XMPP'), _('Client')]
+tags = [_('Web chat'), _('XMPP'), _('Client')]
diff --git a/plinth/modules/kiwix/manifest.py b/plinth/modules/kiwix/manifest.py
index f989f9cd8..536bb30a1 100644
--- a/plinth/modules/kiwix/manifest.py
+++ b/plinth/modules/kiwix/manifest.py
@@ -19,4 +19,9 @@ backup = {
'services': ['kiwix-server-freedombox']
}
-tags = [_('Offline Reader'), _('Archival'), _('Censorship Resistance')]
+tags = [
+ _('Offline reader'),
+ _('Archival'),
+ _('Censorship resistance'),
+ _('Wikipedia')
+]
diff --git a/plinth/modules/matrixsynapse/manifest.py b/plinth/modules/matrixsynapse/manifest.py
index fc7ad7da0..009529ea2 100644
--- a/plinth/modules/matrixsynapse/manifest.py
+++ b/plinth/modules/matrixsynapse/manifest.py
@@ -98,11 +98,9 @@ backup = {
}
tags = [
- _('Chat Room'),
- _('Encrypted Messaging'),
- _('IM'),
- _('Audio Chat'),
- _('Video Chat'),
- _('Matrix'),
- _('VoIP')
+ _('Chat room'),
+ _('Encrypted messaging'),
+ _('Audio chat'),
+ _('Video chat'),
+ _('Matrix server'),
]
diff --git a/plinth/modules/minetest/manifest.py b/plinth/modules/minetest/manifest.py
index 539a8c148..3963d7ce2 100644
--- a/plinth/modules/minetest/manifest.py
+++ b/plinth/modules/minetest/manifest.py
@@ -46,4 +46,4 @@ backup = {
'services': ['minetest-server']
}
-tags = [_('Game'), _('Block Sandbox')]
+tags = [_('Game server'), _('Block sandbox'), _('Platform')]
diff --git a/plinth/modules/minidlna/manifest.py b/plinth/modules/minidlna/manifest.py
index 007e8ce76..8f0f4d3e8 100644
--- a/plinth/modules/minidlna/manifest.py
+++ b/plinth/modules/minidlna/manifest.py
@@ -113,4 +113,4 @@ backup = {
'services': ['minidlna']
}
-tags = [_('Media Server'), _('Television'), _('UPnP'), _('DLNA')]
+tags = [_('Media server'), _('Television'), _('UPnP'), _('DLNA')]
diff --git a/plinth/modules/miniflux/manifest.py b/plinth/modules/miniflux/manifest.py
index f112c564b..7082fb720 100644
--- a/plinth/modules/miniflux/manifest.py
+++ b/plinth/modules/miniflux/manifest.py
@@ -135,4 +135,4 @@ backup = {
'services': ['miniflux']
}
-tags = [_('Feed Reader'), _('News'), _('RSS'), _('ATOM')]
+tags = [_('Feed reader'), _('News aggregation'), _('RSS'), _('ATOM')]
diff --git a/plinth/modules/mumble/manifest.py b/plinth/modules/mumble/manifest.py
index fa7843aed..b51c1d3e9 100644
--- a/plinth/modules/mumble/manifest.py
+++ b/plinth/modules/mumble/manifest.py
@@ -64,4 +64,4 @@ backup = {
'services': ['mumble-server']
}
-tags = [_('Audio Chat'), _('VoIP')]
+tags = [_('Audio chat'), _('Group conference'), _('Server')]
diff --git a/plinth/modules/nextcloud/manifest.py b/plinth/modules/nextcloud/manifest.py
index cf30d69d2..3b1e7172b 100644
--- a/plinth/modules/nextcloud/manifest.py
+++ b/plinth/modules/nextcloud/manifest.py
@@ -53,4 +53,4 @@ backup = {
}
}
-tags = [_('Cloud Storage'), _('File Sharing'), _('non-Debian')]
+tags = [_('File sync'), _('Sharing'), _('Groupware'), _('Non-Debian')]
diff --git a/plinth/modules/openvpn/manifest.py b/plinth/modules/openvpn/manifest.py
index cf7f2b613..68abee13d 100644
--- a/plinth/modules/openvpn/manifest.py
+++ b/plinth/modules/openvpn/manifest.py
@@ -57,4 +57,4 @@ clients = [{
}]
}]
-tags = [_('VPN'), _('Anonymity'), _('Remote Access')]
+tags = [_('VPN server'), _('Privacy'), _('Remote access')]
diff --git a/plinth/modules/privoxy/manifest.py b/plinth/modules/privoxy/manifest.py
index f346fe0a0..1e236221c 100644
--- a/plinth/modules/privoxy/manifest.py
+++ b/plinth/modules/privoxy/manifest.py
@@ -7,4 +7,4 @@ from django.utils.translation import gettext_lazy as _
backup: dict = {}
-tags = [_('Ad Blocker'), _('Proxy'), _('Local Network')]
+tags = [_('Ad blocker'), _('Proxy server'), _('Local network')]
diff --git a/plinth/modules/quassel/manifest.py b/plinth/modules/quassel/manifest.py
index 63aa96256..df7c69112 100644
--- a/plinth/modules/quassel/manifest.py
+++ b/plinth/modules/quassel/manifest.py
@@ -51,4 +51,4 @@ backup = {
'services': ['quasselcore'],
}
-tags = [_('Chat Room'), _('IRC'), _('Client')]
+tags = [_('Chat room'), _('IRC'), _('Client')]
diff --git a/plinth/modules/radicale/manifest.py b/plinth/modules/radicale/manifest.py
index b27bc7523..1a12e0e81 100644
--- a/plinth/modules/radicale/manifest.py
+++ b/plinth/modules/radicale/manifest.py
@@ -88,10 +88,4 @@ backup = {
'services': ['uwsgi']
}
-tags = [
- _('Calendar'),
- _('Contacts'),
- _('Synchronization'),
- _('CalDAV'),
- _('CardDAV')
-]
+tags = [_('Calendar'), _('Contacts'), _('Server'), _('CalDAV'), _('CardDAV')]
diff --git a/plinth/modules/roundcube/manifest.py b/plinth/modules/roundcube/manifest.py
index 919f53bde..5ea6c3e97 100644
--- a/plinth/modules/roundcube/manifest.py
+++ b/plinth/modules/roundcube/manifest.py
@@ -1,4 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
+"""Application manifest for roundcube."""
from django.utils.translation import gettext_lazy as _
@@ -19,4 +20,4 @@ backup = {
}
}
-tags = [_('Email'), _('Contacts'), _('Client')]
+tags = [_('Email'), _('Contacts'), _('Web client')]
diff --git a/plinth/modules/rssbridge/manifest.py b/plinth/modules/rssbridge/manifest.py
index cfe398c9f..cfc2675bc 100644
--- a/plinth/modules/rssbridge/manifest.py
+++ b/plinth/modules/rssbridge/manifest.py
@@ -1,9 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
+"""Application manifest for RSS-Bridge."""
from django.utils.translation import gettext_lazy as _
-"""
-Application manifest for RSS-Bridge.
-"""
clients = [{
'name': _('RSS-Bridge'),
@@ -15,4 +13,4 @@ clients = [{
backup = {'data': {'files': ['/etc/rss-bridge/is_public']}}
-tags = [_('Feed Generator'), _('News'), _('RSS'), _('ATOM')]
+tags = [_('Feed generator'), _('News'), _('RSS'), _('ATOM')]
diff --git a/plinth/modules/samba/manifest.py b/plinth/modules/samba/manifest.py
index 4195c3753..6cdb6994b 100644
--- a/plinth/modules/samba/manifest.py
+++ b/plinth/modules/samba/manifest.py
@@ -85,4 +85,10 @@ clients = [{
backup: dict = {}
-tags = [_('File Sharing'), _('Local Network')]
+tags = [
+ _('File sharing'),
+ _('Local network'),
+ _('Network drive'),
+ _('Media storage'),
+ _('Backup storage')
+]
diff --git a/plinth/modules/searx/manifest.py b/plinth/modules/searx/manifest.py
index 5643f9354..66bd6f1af 100644
--- a/plinth/modules/searx/manifest.py
+++ b/plinth/modules/searx/manifest.py
@@ -14,4 +14,4 @@ PUBLIC_ACCESS_SETTING_FILE = '/etc/searx/allow_public_access'
backup = {'config': {'files': [PUBLIC_ACCESS_SETTING_FILE]}}
-tags = [_('Web Search'), _('Metasearch Engine')]
+tags = [_('Web search'), _('Metasearch Engine')]
diff --git a/plinth/modules/shaarli/manifest.py b/plinth/modules/shaarli/manifest.py
index ae4a70823..6e34a337a 100644
--- a/plinth/modules/shaarli/manifest.py
+++ b/plinth/modules/shaarli/manifest.py
@@ -31,4 +31,4 @@ clients = [{
backup = {'data': {'directories': ['/var/lib/shaarli/data']}}
-tags = [_('Bookmarks'), _('Link Blog'), _('Single User')]
+tags = [_('Bookmarks'), _('Link blog'), _('Single user')]
diff --git a/plinth/modules/shadowsocks/manifest.py b/plinth/modules/shadowsocks/manifest.py
index 5715b73c1..337d5d270 100644
--- a/plinth/modules/shadowsocks/manifest.py
+++ b/plinth/modules/shadowsocks/manifest.py
@@ -15,9 +15,9 @@ backup = {
}
tags = [
- _('Proxy'),
- _('Client'),
- _('SOCKS5'),
- _('Censorship Resistance'),
- _('Shadowsocks')
+ _('Proxy server'),
+ _('Censorship resistance'),
+ _('Encrypted tunnel'),
+ _('Entry point'),
+ _('Shadowsocks'),
]
diff --git a/plinth/modules/shadowsocksserver/manifest.py b/plinth/modules/shadowsocksserver/manifest.py
index 0f34ea395..859e4c55d 100644
--- a/plinth/modules/shadowsocksserver/manifest.py
+++ b/plinth/modules/shadowsocksserver/manifest.py
@@ -14,4 +14,9 @@ backup = {
'services': ['shadowsocks-libev-server@fbxserver']
}
-tags = [_('Proxy'), _('SOCKS5'), _('Censorship Resistance'), _('Shadowsocks')]
+tags = [
+ _('Censorship resistance'),
+ _('Encrypted tunnel'),
+ _('Exit point'),
+ _('Shadowsocks')
+]
diff --git a/plinth/modules/sharing/manifest.py b/plinth/modules/sharing/manifest.py
index f7b839a21..f626db1a7 100644
--- a/plinth/modules/sharing/manifest.py
+++ b/plinth/modules/sharing/manifest.py
@@ -16,4 +16,4 @@ backup = {
}]
}
-tags = [_('File Sharing')]
+tags = [_('File sharing'), _('Web sharing')]
diff --git a/plinth/modules/syncthing/manifest.py b/plinth/modules/syncthing/manifest.py
index 2225a1a69..5b6c132fb 100644
--- a/plinth/modules/syncthing/manifest.py
+++ b/plinth/modules/syncthing/manifest.py
@@ -55,4 +55,4 @@ backup = {
'services': ['syncthing@syncthing']
}
-tags = [_('Synchronization'), _('File Sharing'), _('Cloud Storage')]
+tags = [_('File sync'), _('File sharing'), _('P2P')]
diff --git a/plinth/modules/tiddlywiki/manifest.py b/plinth/modules/tiddlywiki/manifest.py
index d7da627f0..c91dad349 100644
--- a/plinth/modules/tiddlywiki/manifest.py
+++ b/plinth/modules/tiddlywiki/manifest.py
@@ -17,11 +17,11 @@ backup = {'data': {'directories': [str(wiki_dir)]}}
tags = [
_('Wiki'),
- _('Note Taking'),
+ _('Note taking'),
_('Website'),
_('Journal'),
- _('Digital Garden'),
+ _('Digital garden'),
_('Zettelkasten'),
_('Quine'),
- _('non-Debian')
+ _('Non-Debian')
]
diff --git a/plinth/modules/tor/manifest.py b/plinth/modules/tor/manifest.py
index 3d3eafe96..93f70fc8b 100644
--- a/plinth/modules/tor/manifest.py
+++ b/plinth/modules/tor/manifest.py
@@ -54,8 +54,9 @@ backup = {
}
tags = [
+ _('Onion services'),
_('Relay'),
- _('Anonymity Network'),
- _('Censorship Resistance'),
- _('Tor')
+ _('Anonymity network'),
+ _('Censorship resistance'),
+ _('Tor'),
]
diff --git a/plinth/modules/torproxy/manifest.py b/plinth/modules/torproxy/manifest.py
index 6378e8876..7d51fee96 100644
--- a/plinth/modules/torproxy/manifest.py
+++ b/plinth/modules/torproxy/manifest.py
@@ -52,9 +52,8 @@ backup = {
}
tags = [
- _('Proxy'),
- _('SOCKS5'),
- _('Anonymity Network'),
- _('Censorship Resistance'),
+ _('Proxy server'),
+ _('Anonymity network'),
+ _('Censorship resistance'),
_('Tor')
]
diff --git a/plinth/modules/transmission/manifest.py b/plinth/modules/transmission/manifest.py
index b99e3c49c..76e960db3 100644
--- a/plinth/modules/transmission/manifest.py
+++ b/plinth/modules/transmission/manifest.py
@@ -36,4 +36,4 @@ backup = {
'services': ['transmission-daemon']
}
-tags = [_('File Sharing'), _('BitTorrent'), _('Client'), _('P2P')]
+tags = [_('File sharing'), _('BitTorrent'), _('Web client'), _('P2P')]
diff --git a/plinth/modules/ttrss/manifest.py b/plinth/modules/ttrss/manifest.py
index 2510c0d3d..523b85870 100644
--- a/plinth/modules/ttrss/manifest.py
+++ b/plinth/modules/ttrss/manifest.py
@@ -52,4 +52,4 @@ backup = {
'services': ['tt-rss']
}
-tags = [_('Feed Reader'), _('News'), _('RSS'), _('ATOM')]
+tags = [_('Feed reader'), _('News aggregation'), _('RSS'), _('ATOM')]
diff --git a/plinth/modules/wireguard/manifest.py b/plinth/modules/wireguard/manifest.py
index 274783aa3..c7e9732e5 100644
--- a/plinth/modules/wireguard/manifest.py
+++ b/plinth/modules/wireguard/manifest.py
@@ -42,4 +42,4 @@ clients = [{
}]
}]
-tags = [_('VPN'), _('Anonymity'), _('Remote Access'), _('P2P')]
+tags = [_('VPN client'), _('VPN server'), _('Privacy'), _('Remote access')]
diff --git a/plinth/modules/wordpress/manifest.py b/plinth/modules/wordpress/manifest.py
index 1928f26ad..ca90cf852 100644
--- a/plinth/modules/wordpress/manifest.py
+++ b/plinth/modules/wordpress/manifest.py
@@ -23,4 +23,4 @@ backup = {
},
}
-tags = [_('Website'), _('Blog')]
+tags = [_('Website'), _('Blog'), _('Content management system')]
diff --git a/plinth/modules/zoph/manifest.py b/plinth/modules/zoph/manifest.py
index 81680b453..b973c0963 100644
--- a/plinth/modules/zoph/manifest.py
+++ b/plinth/modules/zoph/manifest.py
@@ -23,4 +23,4 @@ backup = {
}
}
-tags = [_('Image Viewer'), _('Photo'), _('Library')]
+tags = [_('Photo'), _('Organizer'), _('Web sharing')]
diff --git a/plinth/tests/tags/test_functional.py b/plinth/tests/tags/test_functional.py
index 85c8bd6f1..11ef3dd35 100644
--- a/plinth/tests/tags/test_functional.py
+++ b/plinth/tests/tags/test_functional.py
@@ -4,8 +4,8 @@ Functional, browser based tests for transmission app.
"""
import pytest
-
from selenium.webdriver.common.keys import Keys
+
from plinth.tests import functional
pytestmark = [pytest.mark.tags]
@@ -39,11 +39,11 @@ def test_search_for_tag(session_browser):
search_input = session_browser.driver.find_element_by_id('add-tag-input')
with functional.wait_for_page_update(
session_browser, timeout=10,
- expected_url='/plinth/apps/?tag=BitTorrent&tag=File%20Sharing'):
+ expected_url='/plinth/apps/?tag=BitTorrent&tag=File%20sharing'):
search_input.click()
search_input.send_keys('file')
search_input.send_keys(Keys.ENTER)
- for app in ['deluge', 'nextcloud', 'sharing', 'syncthing', 'transmission']:
+ for app in ['deluge', 'samba', 'sharing', 'syncthing', 'transmission']:
_is_app_listed(session_browser, app)
@@ -52,9 +52,9 @@ def test_click_on_tag(session_browser):
search_input = session_browser.driver.find_element_by_id('add-tag-input')
with functional.wait_for_page_update(
session_browser, timeout=10,
- expected_url='/plinth/apps/?tag=BitTorrent&tag=Cloud%20Storage'):
+ expected_url='/plinth/apps/?tag=BitTorrent&tag=File%20sync'):
search_input.click()
session_browser.find_by_css(
- ".dropdown-item[data-value='Cloud Storage']").click()
+ ".dropdown-item[data-value='File sync']").click()
for app in ['deluge', 'nextcloud', 'syncthing', 'transmission']:
_is_app_listed(session_browser, app)
From d605907bbec007484f53202caa999eb427183ee9 Mon Sep 17 00:00:00 2001
From: Sunil Mohan Adapa
Date: Tue, 15 Oct 2024 15:45:16 -0700
Subject: [PATCH 36/56] context_processors: Use active menu urls to decide what
to highlight
- We are using submenu.url to check for specific URLs and then highlight a menu
item. This is somewhat incorrect due to string search and not generic enough. We
have another mechanism 'active_menu_urls' to perform this. Improve and use this
instead.
Signed-off-by: Sunil Mohan Adapa
---
plinth/context_processors.py | 4 +++-
plinth/templates/base.html | 8 ++++----
plinth/tests/test_context_processors.py | 12 ++++++------
3 files changed, 13 insertions(+), 11 deletions(-)
diff --git a/plinth/context_processors.py b/plinth/context_processors.py
index 01a4bbf8b..ddfc2af0f 100644
--- a/plinth/context_processors.py
+++ b/plinth/context_processors.py
@@ -27,7 +27,9 @@ def common(request):
request, user=request.user)
slash_indices = [match.start() for match in re.finditer('/', request.path)]
- active_menu_urls = [request.path[:index + 1] for index in slash_indices]
+ active_menu_urls = [
+ request.path[:index + 1] for index in slash_indices[2:]
+ ] # Ignore the first two slashes '/plinth/apps/'
return {
'cfg': cfg,
'submenu': menu.main_menu.active_item(request),
diff --git a/plinth/templates/base.html b/plinth/templates/base.html
index eb91fc4b6..680d93184 100644
--- a/plinth/templates/base.html
+++ b/plinth/templates/base.html
@@ -83,7 +83,7 @@
{% block mainmenu_left %}
@@ -110,7 +110,7 @@
{% url 'index' as index_url %}
@@ -118,7 +118,7 @@
{% url 'apps' as apps_url %}