ui: tags: Add tag search/filter for system page

Tests:

- In apps page, notice that all the tags are displayed as expected.

- Inside an app from apps sections, clicking on an tag shows the apps with that
tag filtered.

- Clicking on the search bar shows the list of all tags.

- Clicking on tag from search list adds that tag to the search list.

- Labels are shown properly in the search bar.

- Clicking on label removes it from search.

- Search results are sorted based on the number of matches.

- Clicking on the close button the tags search input removes filtering.

- All the above tests work for systems page with systems app. Sections are shown
even when apps are filtered by tags. Sections without results are not shown.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2025-03-03 19:18:56 -08:00 committed by James Valleroy
parent 9555697140
commit a5ab31c1af
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
2 changed files with 63 additions and 17 deletions

View File

@ -6,6 +6,10 @@
{% load static %}
{% load i18n %}
{% block page_js %}
<script type="text/javascript" src="{% static 'tags.js' %}" defer></script>
{% endblock %}
{% block body_class %}system-page{% endblock %}
{% block container %}
@ -17,12 +21,16 @@
</div>
</div>
{% block tags %}
{% include "tags.html" %}
{% endblock %}
<div class="container card-container">
{% for section_item in menu_items %}
<div class="system-section-title">{{ section_item.name }}</div>
<div class="row">
<div class="card-list card-list-primary">
{% for item in section_item.sorted_items %}
{% for item in section_item.items %}
{% if advanced_mode or not item.advanced %}
{% include "card.html" %}
{% endif %}

View File

@ -174,13 +174,30 @@ def index(request):
def _pick_menu_items(menu_items, selected_tags):
"""Return a sorted list of menu items filtered by tags."""
class MenuProxy:
"""A proxy for the menu item to hold filtered children."""
def __init__(self, menu_item: menu.Menu):
"""Initialize a menu proxy object."""
self.menu_item = menu_item
self.items: list[menu.Menu] = []
tags = menu_item.tags or []
for item in menu_item.items:
tags += item.tags or []
self.tags = list(tags)
def __getattr__(self, name: str):
"""Return attributed from proxied object."""
return getattr(self.menu_item, name)
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.tags)
menu_tags = set(menu_item.tags or [])
return [tag not in menu_tags for tag in selected_tags]
def _sort_key(menu_item):
@ -194,17 +211,39 @@ def _pick_menu_items(menu_items, selected_tags):
return (_mismatch_map(menu_item).count(True), _mismatch_map(menu_item),
menu_item.order, menu_item.name.lower())
proxied_menu_items = []
for menu_item in menu_items:
proxied_item = MenuProxy(menu_item)
proxied_item.items = _pick_menu_items(menu_item.items, selected_tags)
proxied_menu_items.append(proxied_item)
# 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
menu_item for menu_item in proxied_menu_items
if (not selected_tags) or (not all(_mismatch_map(menu_item)))
]
return sorted(filtered_menu_items, key=_sort_key)
def _get_all_tags(menu_items: list[menu.Menu]) -> list[str]:
"""Return a sorted list of all tags present in the given menu items."""
def get_tags(menu_items: list[menu.Menu]) -> set[str]:
"""Return a list of tags, unsorted."""
all_tags = set()
for menu_item in menu_items:
all_tags.update(menu_item.tags or [])
all_tags |= get_tags(menu_item.items)
return all_tags
# Sort tags by localized string
return sorted(get_tags(menu_items), key=_)
class AppsIndexView(TemplateView):
"""View for apps index.
@ -223,12 +262,7 @@ class AppsIndexView(TemplateView):
menu_items = menu.main_menu.active_item(self.request).items
context['tags'] = tags
# Sorted tags by localized string
all_tags = set()
for menu_item in menu_items:
all_tags.update(menu_item.tags or [])
context['all_tags'] = sorted(all_tags, key=_)
context['all_tags'] = _get_all_tags(menu_items)
context['menu_items'] = _pick_menu_items(menu_items, tags)
return context
@ -236,11 +270,16 @@ class AppsIndexView(TemplateView):
def system_index(request):
"""Serve the system index page."""
menu_items = menu.main_menu.active_item(request).sorted_items()
return TemplateResponse(request, 'system.html', {
'advanced_mode': get_advanced_mode(),
'menu_items': menu_items
})
tags = request.GET.getlist('tag', [])
menu_items = menu.main_menu.active_item(request).items
return TemplateResponse(
request, 'system.html', {
'advanced_mode': get_advanced_mode(),
'menu_items': _pick_menu_items(menu_items, tags),
'tags': tags,
'all_tags': _get_all_tags(menu_items)
})
class LanguageSelectionView(FormView):
@ -471,9 +510,8 @@ 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']: