diff --git a/scancodeio/static/main.css b/scancodeio/static/main.css index bb1622b852..b96ddd3eb2 100644 --- a/scancodeio/static/main.css +++ b/scancodeio/static/main.css @@ -557,3 +557,22 @@ body.full-screen #resource-viewer .message-header { background-color: var(--bulma-background); border-radius: var(--bulma-radius); } +#resource-tree-container .search-container { + position: sticky; + top: 0; + z-index: 100; +} +#resource-tree-container .search-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1000; + border: 1px solid var(--bulma-border); + border-radius: var(--bulma-radius); + background: var(--bulma-scheme-main); + box-shadow: var(--bulma-shadow); + max-height: 400px; + overflow-y: auto; + margin-top: 4px; +} diff --git a/scanpipe/templates/scanpipe/panels/resource_search_results.html b/scanpipe/templates/scanpipe/panels/resource_search_results.html new file mode 100644 index 0000000000..08f7ca65a5 --- /dev/null +++ b/scanpipe/templates/scanpipe/panels/resource_search_results.html @@ -0,0 +1,30 @@ +{% if search_results %} +
+ {% for resource in search_results %} + + {% endfor %} +
+{% elif query %} +
+
+ +
+

No files found matching "{{ query }}"

+
+{% endif %} \ No newline at end of file diff --git a/scanpipe/templates/scanpipe/resource_tree.html b/scanpipe/templates/scanpipe/resource_tree.html index 660db165cf..603cd2fdbc 100644 --- a/scanpipe/templates/scanpipe/resource_tree.html +++ b/scanpipe/templates/scanpipe/resource_tree.html @@ -15,6 +15,35 @@
+
+
+
+ + + + +
+
+ +
+
+ +
{% include "scanpipe/panels/codebase_tree_panel.html" with children=children path=path %}
@@ -135,6 +164,72 @@ document.body.style.userSelect = ''; } }); + + const searchInput = document.getElementById('file-search-input'); + const searchResults = document.getElementById('search-results'); + const clearSearchBtn = document.getElementById('clear-search'); + + function toggleSearchResults(show = null) { + const shouldShow = show !== null ? show : searchInput.value.trim(); + searchResults.classList.toggle('is-hidden', !shouldShow); + } + + function clearSearch() { + searchInput.value = ''; + toggleSearchResults(false); + searchInput.focus(); + } + + function handleSearchResultClick(searchResultItem) { + const path = searchResultItem.dataset.path; + + toggleSearchResults(false); + searchInput.blur(); + + fetch(`{% url 'codebase_resource_table' project.slug %}?path=${encodeURIComponent(path)}`) + .then(response => response.text()) + .then(html => { + document.getElementById('right-pane').innerHTML = html; + htmx.process(document.getElementById('right-pane')); + if (typeof enableCopyToClipboard === 'function') { + enableCopyToClipboard('.copy-to-clipboard'); + } + const newUrl = `{% url 'codebase_resource_tree' project.slug %}?path=${encodeURIComponent(path)}`; + window.history.pushState(null, '', newUrl); + expandToPath(path); + }); + } + + searchInput.addEventListener('focus', () => toggleSearchResults()); + searchInput.addEventListener('input', () => toggleSearchResults()); + + clearSearchBtn.addEventListener('click', clearSearch); + + searchInput.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + toggleSearchResults(false); + searchInput.blur(); + } + }); + + document.addEventListener('click', function(e) { + const searchResultItem = e.target.closest('.dropdown-item'); + if (searchResultItem) { + e.preventDefault(); + handleSearchResultClick(searchResultItem); + return; + } + + if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) { + toggleSearchResults(false); + } + }); + + document.body.addEventListener('htmx:afterSettle', function(evt) { + if (evt.target === searchResults) { + toggleSearchResults(); + } + }); }); {% endblock %} \ No newline at end of file diff --git a/scanpipe/urls.py b/scanpipe/urls.py index 67a457f94d..e5605f3063 100644 --- a/scanpipe/urls.py +++ b/scanpipe/urls.py @@ -136,6 +136,11 @@ views.CodebaseResourceTreeView.as_view(), name="codebase_resource_tree", ), + path( + "project//resource_search/", + views.CodebaseResourceSearchView.as_view(), + name="codebase_resource_search", + ), path( "project//resource_table/", views.CodebaseResourceTableView.as_view(), diff --git a/scanpipe/views.py b/scanpipe/views.py index bfd18ca158..7c351369de 100644 --- a/scanpipe/views.py +++ b/scanpipe/views.py @@ -2795,6 +2795,28 @@ def get(self, request, *args, **kwargs): return render(request, self.template_name, context) +class CodebaseResourceSearchView( + ConditionalLoginRequired, + ProjectRelatedViewMixin, + generic.ListView, +): + model = CodebaseResource + template_name = "scanpipe/panels/resource_search_results.html" + context_object_name = "search_results" + paginate_by = 30 + + def get_queryset(self): + qs = super().get_queryset() + search_query = self.request.GET.get("search", "").strip() + qs = qs.filter(path__icontains=search_query) + return qs.only("path", "type", "name").order_by("path") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["query"] = self.request.GET.get("search", "") + return context + + class CodebaseResourceTableView( ConditionalLoginRequired, ProjectRelatedViewMixin,