Skip to content

Commit a2dd550

Browse files
committed
add basic search to resource tree
Signed-off-by: Aayush Kumar <[email protected]>
1 parent 2da4d37 commit a2dd550

File tree

4 files changed

+170
-0
lines changed

4 files changed

+170
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{% if search_results %}
2+
<div class="search-results">
3+
{% for resource in search_results %}
4+
<div class="dropdown-item p-2 is-clickable"
5+
data-path="{{ resource.path }}"
6+
data-is-dir="{% if resource.is_dir %}true{% else %}false{% endif %}">
7+
<div class="is-flex is-align-items-center">
8+
<span class="icon is-small mr-2">
9+
{% if resource.is_dir %}
10+
<i class="fas fa-folder"></i>
11+
{% else %}
12+
<i class="far fa-file"></i>
13+
{% endif %}
14+
</span>
15+
<div class="is-flex-grow-1">
16+
<div class="has-text-weight-semibold">{{ resource.name }}</div>
17+
<div class="has-text-grey is-size-7 break-all">{{ resource.path }}</div>
18+
</div>
19+
</div>
20+
</div>
21+
{% endfor %}
22+
</div>
23+
{% elif query %}
24+
<div class="has-text-centered p-4">
25+
<div class="icon is-large has-text-grey-light mb-2">
26+
<i class="fas fa-search fa-2x"></i>
27+
</div>
28+
<p class="has-text-grey">No files found matching "{{ query }}"</p>
29+
</div>
30+
{% endif %}

scanpipe/templates/scanpipe/resource_tree.html

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,25 @@
8383
background-color: var(--bulma-background);
8484
border-radius: var(--bulma-radius);
8585
}
86+
.search-container {
87+
position: sticky;
88+
top: 0;
89+
z-index: 100;
90+
}
91+
.search-dropdown {
92+
position: absolute;
93+
top: 100%;
94+
left: 0;
95+
right: 0;
96+
z-index: 1000;
97+
border: 1px solid var(--bulma-border);
98+
border-radius: var(--bulma-radius);
99+
background: var(--bulma-scheme-main);
100+
box-shadow: var(--bulma-shadow);
101+
max-height: 400px;
102+
overflow-y: auto;
103+
margin-top: 4px;
104+
}
86105
</style>
87106
{% endblock %}
88107

@@ -98,6 +117,36 @@
98117

99118
<div class="resizable-container">
100119
<div id="left-pane" class="left-pane px-2">
120+
<div class="mb-3 search-container">
121+
<div class="field has-addons">
122+
<div class="control has-icons-left is-expanded">
123+
<input
124+
id="file-search-input"
125+
class="input is-small"
126+
type="text"
127+
placeholder="Go to file..."
128+
autocomplete="off"
129+
hx-get="{% url 'codebase_resource_search' project.slug %}"
130+
hx-target="#search-results"
131+
hx-trigger="input changed"
132+
hx-include="this"
133+
name="search"
134+
>
135+
<span class="icon is-small is-left">
136+
<i class="fas fa-search"></i>
137+
</span>
138+
</div>
139+
<div class="control">
140+
<button id="clear-search" class="button is-small" type="button">
141+
<span class="icon is-small">
142+
<i class="fas fa-times"></i>
143+
</span>
144+
</button>
145+
</div>
146+
</div>
147+
<div id="search-results" class="search-dropdown is-hidden"></div>
148+
</div>
149+
101150
<div id="resource-tree">
102151
{% include "scanpipe/panels/codebase_tree_panel.html" with children=children path=path %}
103152
</div>
@@ -219,6 +268,70 @@
219268
document.body.style.userSelect = '';
220269
}
221270
});
271+
272+
const searchInput = document.getElementById('file-search-input');
273+
const searchResults = document.getElementById('search-results');
274+
const clearSearchBtn = document.getElementById('clear-search');
275+
276+
function toggleSearchResults(show = null) {
277+
const shouldShow = show !== null ? show : searchInput.value.trim();
278+
searchResults.classList.toggle('is-hidden', !shouldShow);
279+
}
280+
281+
function clearSearch() {
282+
searchInput.value = '';
283+
toggleSearchResults(false);
284+
searchInput.focus();
285+
}
286+
287+
function handleSearchResultClick(searchResultItem) {
288+
const path = searchResultItem.dataset.path;
289+
290+
clearSearch();
291+
292+
fetch(`{% url 'codebase_resource_table' project.slug %}?path=${encodeURIComponent(path)}`)
293+
.then(response => response.text())
294+
.then(html => {
295+
document.getElementById('right-pane').innerHTML = html;
296+
htmx.process(document.getElementById('right-pane'));
297+
if (typeof enableCopyToClipboard === 'function') {
298+
enableCopyToClipboard('.copy-to-clipboard');
299+
}
300+
const newUrl = `{% url 'codebase_resource_tree' project.slug %}?path=${encodeURIComponent(path)}`;
301+
window.history.pushState(null, '', newUrl);
302+
expandToPath(path);
303+
});
304+
}
305+
306+
searchInput.addEventListener('focus', () => toggleSearchResults());
307+
searchInput.addEventListener('input', () => toggleSearchResults());
308+
309+
clearSearchBtn.addEventListener('click', clearSearch);
310+
311+
searchInput.addEventListener('keydown', function(e) {
312+
if (e.key === 'Escape') {
313+
clearSearch();
314+
}
315+
});
316+
317+
document.addEventListener('click', function(e) {
318+
const searchResultItem = e.target.closest('.dropdown-item');
319+
if (searchResultItem) {
320+
e.preventDefault();
321+
handleSearchResultClick(searchResultItem);
322+
return;
323+
}
324+
325+
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
326+
toggleSearchResults(false);
327+
}
328+
});
329+
330+
document.body.addEventListener('htmx:afterSettle', function(evt) {
331+
if (evt.target === searchResults) {
332+
toggleSearchResults();
333+
}
334+
});
222335
});
223336
</script>
224337
{% endblock %}

scanpipe/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@
136136
views.CodebaseResourceTreeView.as_view(),
137137
name="codebase_resource_tree",
138138
),
139+
path(
140+
"project/<slug:slug>/resource_search/",
141+
views.CodebaseResourceSearchView.as_view(),
142+
name="codebase_resource_search",
143+
),
139144
path(
140145
"project/<slug:slug>/resource_table/",
141146
views.CodebaseResourceTableView.as_view(),

scanpipe/views.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2790,6 +2790,28 @@ def get(self, request, *args, **kwargs):
27902790
return render(request, self.template_name, context)
27912791

27922792

2793+
class CodebaseResourceSearchView(
2794+
ConditionalLoginRequired,
2795+
ProjectRelatedViewMixin,
2796+
generic.ListView,
2797+
):
2798+
model = CodebaseResource
2799+
template_name = "scanpipe/panels/resource_search_results.html"
2800+
context_object_name = "search_results"
2801+
paginate_by = 30
2802+
2803+
def get_queryset(self):
2804+
qs = super().get_queryset()
2805+
search_query = self.request.GET.get("search", "").strip()
2806+
qs = qs.filter(path__icontains=search_query)
2807+
return qs.only("path", "type", "name").order_by("path")
2808+
2809+
def get_context_data(self, **kwargs):
2810+
context = super().get_context_data(**kwargs)
2811+
context["query"] = self.request.GET.get("search", "")
2812+
return context
2813+
2814+
27932815
class CodebaseResourceTableView(
27942816
ConditionalLoginRequired,
27952817
ProjectRelatedViewMixin,

0 commit comments

Comments
 (0)