Skip to content

Commit 5dfc900

Browse files
committed
Global admin search
1 parent 30e0484 commit 5dfc900

File tree

10 files changed

+273
-162
lines changed

10 files changed

+273
-162
lines changed

bolt-admin/bolt/admin/templates/admin/base.html

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
{%- endif -%}
1212
</title>
1313
{% tailwind_css %}
14+
{% htmx_js %}
1415
<link href="{{ asset('admin/admin.css') }}" rel="stylesheet">
1516
<script src="{{ asset('admin/jquery-3.6.1.slim.min.js') }}"></script>
1617
<script src="{{ asset('admin/chart.js') }}" defer></script>
@@ -83,7 +84,27 @@
8384
<span class="text-stone-400">/</span>
8485
<a class="text-stone-600" href="{{ request.path }}">{{ title }}</a>
8586
</div>
86-
<div class="flex items-center">
87+
<div class="flex items-center space-x-5">
88+
<div class="flex justify-end">
89+
<form method="GET" action="{{ url('admin:search') }}" class="flex">
90+
<div class="relative max-w-xs">
91+
<label for="query" class="sr-only">Search</label>
92+
<input
93+
type="text"
94+
name="query"
95+
id="query"
96+
class="block w-full px-3 pl-10 text-sm border-gray-200 rounded-md focus:border-blue-500 focus:ring-blue-500"
97+
placeholder="Search everything"
98+
value="{{ global_search_query|default('') }}"
99+
>
100+
<div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
101+
<svg class="h-3.5 w-3.5 text-gray-400" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
102+
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"></path>
103+
</svg>
104+
</div>
105+
</div>
106+
</form>
107+
</div>
87108
<a href="/">Back to app</a>
88109
</div>
89110
</div>
@@ -92,7 +113,7 @@
92113
<div>
93114
{% block header %}
94115
<h1 class="text-2xl font-semibold text-stone-700">
95-
{{ title }}
116+
{% block title %}{{ title }}{% endblock %}
96117
</h1>
97118
{% if description %}
98119
<p class="mt-2 text-sm text-gray-500">{{ description }}</p>

bolt-admin/bolt/admin/templates/admin/list.html

Lines changed: 166 additions & 153 deletions
Large diffs are not rendered by default.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{% extends "admin/base.html" %}
2+
3+
{% block title %}
4+
{%- if global_search_query -%}
5+
Search results for "{{ global_search_query }}"
6+
{%- else -%}
7+
Search
8+
{%- endif -%}
9+
{% endblock %}
10+
11+
{% block content %}
12+
13+
{% if global_search_query %}
14+
<div class="mt-6">
15+
{% for view in searchable_views %}
16+
<div
17+
class="*:mb-14"
18+
hx-get="{{ view.get_absolute_url() }}?search={{ global_search_query }}&page_size=5"
19+
hx-trigger="bhxLoad from:body"
20+
bhx-fragment="list">
21+
</div>
22+
{% endfor %}
23+
</div>
24+
{% else %}
25+
<p class="text-stone-500">Enter a search query in the top bar</p>
26+
{% endif %}
27+
28+
{% endblock %}

bolt-admin/bolt/admin/urls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from bolt.urls import include, path
22

3-
from .views.default import AdminIndexView
3+
from .views.default import AdminIndexView, AdminSearchView
44
from .views.registry import registry
55

66
default_namespace = "admin"
77

88

99
urlpatterns = [
10+
path("search/", AdminSearchView.as_view(), name="search"),
1011
path("", include(registry.get_urls())),
1112
path("", AdminIndexView.as_view(), name="index"),
1213
]

bolt-admin/bolt/admin/views/base.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import TYPE_CHECKING
22

33
from bolt.db import models
4+
from bolt.htmx.views import HTMXViewMixin
45
from bolt.http import HttpResponse, HttpResponseRedirect
56
from bolt.paginator import Paginator
67
from bolt.urls import reverse
@@ -115,25 +116,28 @@ def render_card(self, card: "Card"):
115116
return card().render(self.request)
116117

117118

118-
class AdminListView(AdminView):
119+
class AdminListView(HTMXViewMixin, AdminView):
119120
template_name = "admin/list.html"
120121
fields: list[str]
121122
actions: list[str] = []
122123
filters: list[str] = []
123124
page_size = 100
124125
show_search = False
126+
allow_global_search = False
125127

126128
def get_context(self):
127129
context = super().get_context()
128130

129131
# Make this available on self for usage in get_objects and other methods
130132
self.filter = self.request.GET.get("filter", "")
131133

132-
objects = self.get_objects()
134+
page_size = self.request.GET.get("page_size", self.page_size)
135+
paginator = Paginator(self.get_objects(), page_size)
136+
self._page = paginator.get_page(self.request.GET.get("page", 1))
133137

134-
context["paginator"] = Paginator(objects, self.page_size)
135-
context["page"] = context["paginator"].get_page(self.request.GET.get("page", 1))
136-
context["objects"] = context["page"] # alias
138+
context["paginator"] = paginator
139+
context["page"] = self._page
140+
context["objects"] = self._page # alias
137141
context["fields"] = self.get_fields()
138142
context["actions"] = self.get_actions()
139143
context["filters"] = self.get_filters()
@@ -144,6 +148,8 @@ def get_context(self):
144148
context["search_query"] = self.request.GET.get("search", "")
145149
context["show_search"] = self.show_search
146150

151+
context["table_style"] = getattr(self, "_table_style", "default")
152+
147153
context["get_object_pk"] = self.get_object_pk
148154
context["get_object_field"] = self.get_object_field
149155

@@ -153,6 +159,24 @@ def get_context(self):
153159

154160
return context
155161

162+
def get(self) -> HttpResponse:
163+
if self.is_htmx_request:
164+
hx_from_this_page = self.request.path in self.request.headers.get(
165+
"HX-Current-Url", ""
166+
)
167+
if not hx_from_this_page:
168+
self._table_style = "simple"
169+
else:
170+
hx_from_this_page = False
171+
172+
response = super().get()
173+
174+
if self.is_htmx_request and not hx_from_this_page and not self._page:
175+
# Don't render anything
176+
return HttpResponse(status=204)
177+
178+
return response
179+
156180
def post(self) -> HttpResponse:
157181
# won't be "key" anymore, just list
158182
action_name = self.request.POST.get("action_name")

bolt-admin/bolt/admin/views/default.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,15 @@ def get_context(self):
2222
context = super().get_context()
2323
context["dashboards"] = registry.registered_dashboards
2424
return context
25+
26+
27+
class AdminSearchView(AdminView):
28+
template_name = "admin/search.html"
29+
title = "Search"
30+
slug = "search"
31+
32+
def get_context(self):
33+
context = super().get_context()
34+
context["searchable_views"] = registry.get_searchable_views()
35+
context["global_search_query"] = self.request.GET.get("query", "")
36+
return context

bolt-admin/bolt/admin/views/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def get_model_field(instance, field):
3636

3737
class AdminModelListView(AdminListView):
3838
show_search = True
39+
allow_global_search = True
3940

4041
model: "models.Model"
4142

bolt-admin/bolt/admin/views/registry.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ def add_view_path(view, _path):
9696

9797
return urlpatterns
9898

99+
def get_searchable_views(self):
100+
views = [
101+
view
102+
for view in self.registered_views
103+
if getattr(view, "allow_global_search", False)
104+
]
105+
views.sort(key=lambda v: v.get_title())
106+
return views
107+
99108

100109
registry = AdminViewRegistry()
101110
register_view = registry.register_view

bolt-admin/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ license = "MIT"
1313

1414
[tool.poetry.dependencies]
1515
python = "^3.8"
16+
# TODO bolt-htmx required
1617

1718
[tool.poetry.dev-dependencies]
1819
pytest = "^7.1.2"

bolt-htmx/bolt/htmx/assets/htmx/bolthtmx.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,6 @@ htmx.defineExtension("error-classes", {
4848
});
4949

5050
// Our own load event, to support lazy loading
51-
// *after* our fragment extension is added
51+
// *after* our fragment extension is added.
52+
// Use with hx-trigger="bhxLoad from:body"
5253
htmx.trigger(document.body, "bhxLoad");

0 commit comments

Comments
 (0)