Fix prefetch
This commit is contained in:
@@ -41,6 +41,7 @@ from common.components.primitives import (
|
||||
paginated_table_content,
|
||||
)
|
||||
from common.components.search_select import (
|
||||
DEFAULT_PREFETCH,
|
||||
FilterSelect,
|
||||
LabeledOption,
|
||||
SearchSelect,
|
||||
@@ -87,6 +88,7 @@ __all__ = [
|
||||
"Popover",
|
||||
"PopoverTruncated",
|
||||
"SearchField",
|
||||
"DEFAULT_PREFETCH",
|
||||
"FilterSelect",
|
||||
"LabeledOption",
|
||||
"SearchSelect",
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.utils.safestring import SafeText, mark_safe
|
||||
|
||||
from common.components.core import Component
|
||||
from common.components.primitives import Label, Span
|
||||
from common.components.search_select import FilterSelect, LabeledOption
|
||||
from common.components.search_select import DEFAULT_PREFETCH, FilterSelect, LabeledOption
|
||||
|
||||
|
||||
class FilterChoice(NamedTuple):
|
||||
@@ -91,8 +91,6 @@ def _parse_bool(existing: dict, key: str) -> bool:
|
||||
# option set; model-backed fields fetch from a search endpoint on demand, with
|
||||
# labels embedded in the filter JSON so pills render without a DB round-trip.
|
||||
|
||||
_FILTER_PREFETCH = 20
|
||||
|
||||
# Presence modifiers drive the pinned (Any)/(None) pseudo-options. They are
|
||||
# mutually exclusive with value pills (selecting one clears the value set).
|
||||
# Must match JS PRESENCE_MODIFIERS in search_select.js.
|
||||
@@ -189,7 +187,7 @@ def _model_filter(
|
||||
modifier=modifier,
|
||||
modifier_options=_modifier_options(nullable, m2m_modifiers),
|
||||
search_url=search_url,
|
||||
prefetch=_FILTER_PREFETCH,
|
||||
prefetch=DEFAULT_PREFETCH,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -67,6 +67,11 @@ _NO_RESULTS_CLASS = "px-3 py-2 text-sm italic text-body hidden"
|
||||
# used to derive the panel's max-height from items_visible.
|
||||
_ROW_HEIGHT_REM = 2.25
|
||||
|
||||
# Default number of rows to fetch on first focus when a search_url is set.
|
||||
# Shared by filter and form widgets so the dropdown is populated for keyboard
|
||||
# navigation as soon as the user opens it.
|
||||
DEFAULT_PREFETCH = 20
|
||||
|
||||
# ── FilterSelect styling ───────────────────────────────────────────────────
|
||||
# Inline class strings (ported verbatim from the retired SelectableFilter CSS)
|
||||
# so the filter combobox is fully self-styled — nothing in input.css. JS-added
|
||||
|
||||
+2
-2
@@ -206,8 +206,8 @@ textarea:disabled {
|
||||
label {
|
||||
@apply mb-2.5 text-sm font-medium text-heading;
|
||||
}
|
||||
input:not([type="checkbox"]) {
|
||||
@apply mb-3 bg-neutral-secondary-medium border border-default-medium text-heading text-sm rounded-base focus:ring-brand focus:border-brand block w-full px-3 py-2.5 shadow-xs placeholder:text-body;
|
||||
input:not([type="checkbox"]):not([data-search-select-search]) {
|
||||
@apply mb-3 bg-neutral-secondary-medium border border-default-medium text-heading text-sm rounded-base focus:ring-brand focus:border-brand block w-full px-3 py-2.5 shadow-xs placeholder:text-body;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
@apply w-4 h-4 border border-default-medium rounded-xs bg-neutral-secondary-medium focus:ring-2 focus:ring-brand-soft;
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.db import transaction
|
||||
from django.db.models import OuterRef, Subquery
|
||||
|
||||
from common.components import (
|
||||
DEFAULT_PREFETCH,
|
||||
SearchSelect,
|
||||
SearchSelectOption,
|
||||
searchselect_selected,
|
||||
@@ -75,6 +76,7 @@ class SearchSelectWidget(forms.Widget):
|
||||
multi_select=False,
|
||||
items_visible=5,
|
||||
items_scroll=10,
|
||||
prefetch=DEFAULT_PREFETCH,
|
||||
always_visible=False,
|
||||
placeholder="Search…",
|
||||
attrs=None,
|
||||
@@ -85,6 +87,7 @@ class SearchSelectWidget(forms.Widget):
|
||||
self.multi_select = multi_select
|
||||
self.items_visible = items_visible
|
||||
self.items_scroll = items_scroll
|
||||
self.prefetch = prefetch
|
||||
self.always_visible = always_visible
|
||||
self.placeholder = placeholder
|
||||
|
||||
@@ -107,6 +110,7 @@ class SearchSelectWidget(forms.Widget):
|
||||
multi_select=self.multi_select,
|
||||
items_visible=self.items_visible,
|
||||
items_scroll=self.items_scroll,
|
||||
prefetch=self.prefetch,
|
||||
always_visible=self.always_visible,
|
||||
placeholder=self.placeholder,
|
||||
id=(attrs or {}).get("id", ""),
|
||||
|
||||
@@ -4370,7 +4370,7 @@ form input:disabled, select:disabled, textarea:disabled {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-heading);
|
||||
}
|
||||
input:not([type="checkbox"]) {
|
||||
input:not([type="checkbox"]):not([data-search-select-search]) {
|
||||
margin-bottom: calc(var(--spacing) * 3);
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
@@ -60,15 +60,29 @@
|
||||
var pendingRequest = null; // in-flight AbortController, so newer queries win
|
||||
var hasPrefetched = false;
|
||||
|
||||
function hasVisibleContent() {
|
||||
var optionRows = options.querySelectorAll("[data-search-select-option]");
|
||||
for (var i = 0; i < optionRows.length; i++) {
|
||||
if (optionRows[i].style.display !== "none") return true;
|
||||
}
|
||||
if (noResults && !noResults.classList.contains("hidden")) return true;
|
||||
if (options.querySelector("[data-search-select-modifier-option]")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function showPanel() {
|
||||
options.classList.remove("hidden");
|
||||
if (alwaysVisible || hasVisibleContent()) {
|
||||
options.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
function hidePanel() {
|
||||
if (!alwaysVisible) options.classList.add("hidden");
|
||||
}
|
||||
|
||||
function setNoResults(visible) {
|
||||
if (noResults) noResults.classList.toggle("hidden", !visible);
|
||||
if (!noResults) return;
|
||||
noResults.classList.toggle("hidden", !visible);
|
||||
if (visible) showPanel();
|
||||
}
|
||||
|
||||
// ── Highlight tracking (filter mode) ──
|
||||
@@ -198,7 +212,7 @@
|
||||
renderRows(items);
|
||||
// Re-apply the live query: the box may hold more text than was sent.
|
||||
setNoResults(filterRows(search.value.trim()) === 0);
|
||||
if (isFilter) autoHighlight(search.value.trim());
|
||||
autoHighlight(search.value.trim());
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (error && error.name === "AbortError") return; // superseded
|
||||
@@ -214,7 +228,6 @@
|
||||
// so the client-side filter is authoritative.
|
||||
function runSearch() {
|
||||
var query = search.value.trim();
|
||||
showPanel();
|
||||
if (searchUrl) {
|
||||
filterRows(query);
|
||||
setNoResults(false);
|
||||
@@ -225,7 +238,8 @@
|
||||
} else {
|
||||
setNoResults(filterRows(query) === 0);
|
||||
}
|
||||
if (isFilter) autoHighlight(query);
|
||||
autoHighlight(query);
|
||||
showPanel();
|
||||
}
|
||||
|
||||
// ── Single-select combobox: the search box shows the committed label;
|
||||
@@ -238,7 +252,6 @@
|
||||
search.value = "";
|
||||
container._searchSelectDirty = false;
|
||||
}
|
||||
showPanel();
|
||||
if (searchUrl) {
|
||||
if (prefetch && !hasPrefetched) {
|
||||
// Seed the window immediately on first open (not debounced).
|
||||
@@ -248,12 +261,13 @@
|
||||
// Show whatever is already loaded; the server decides no-results.
|
||||
filterRows(search.value.trim());
|
||||
setNoResults(false);
|
||||
if (isFilter) autoHighlight(search.value.trim());
|
||||
autoHighlight(search.value.trim());
|
||||
}
|
||||
} else {
|
||||
setNoResults(filterRows(search.value.trim()) === 0);
|
||||
if (isFilter) autoHighlight(search.value.trim());
|
||||
autoHighlight(search.value.trim());
|
||||
}
|
||||
showPanel();
|
||||
});
|
||||
search.addEventListener("input", function () {
|
||||
clearHighlight();
|
||||
@@ -278,42 +292,42 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ── Keyboard navigation (filter mode) ──
|
||||
// ── Keyboard navigation (both form and filter modes) ──
|
||||
search.addEventListener("keydown", function (event) {
|
||||
if (!isFilter) return;
|
||||
var key = event.key;
|
||||
if (key === "ArrowDown" || key === "ArrowUp" || key === "Enter" || key === "Escape") {
|
||||
var visible = getVisibleOptions();
|
||||
if (visible.length === 0) {
|
||||
if (key === "Escape") hidePanel();
|
||||
return;
|
||||
}
|
||||
if (key !== "ArrowDown" && key !== "ArrowUp" && key !== "Enter" && key !== "Escape") return;
|
||||
var visible = getVisibleOptions();
|
||||
if (visible.length === 0) {
|
||||
if (key === "Escape") hidePanel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "ArrowDown") {
|
||||
if (key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
showPanel();
|
||||
var downIdx = highlightedRow ? visible.indexOf(highlightedRow) : -1;
|
||||
highlightOption(visible[(downIdx + 1) % visible.length]);
|
||||
} else if (key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
showPanel();
|
||||
var upIdx = highlightedRow ? visible.indexOf(highlightedRow) : -1;
|
||||
highlightOption(visible[(upIdx - 1 + visible.length) % visible.length]);
|
||||
} else if (key === "Enter") {
|
||||
if (highlightedRow) {
|
||||
event.preventDefault();
|
||||
showPanel();
|
||||
var idx = highlightedRow ? visible.indexOf(highlightedRow) : -1;
|
||||
var next = visible[(idx + 1) % visible.length];
|
||||
highlightOption(next);
|
||||
} else if (key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
showPanel();
|
||||
var idx = highlightedRow ? visible.indexOf(highlightedRow) : -1;
|
||||
var prev = visible[(idx - 1 + visible.length) % visible.length];
|
||||
highlightOption(prev);
|
||||
} else if (key === "Enter") {
|
||||
if (highlightedRow) {
|
||||
event.preventDefault();
|
||||
var option = optionFromRow(highlightedRow);
|
||||
var option = optionFromRow(highlightedRow);
|
||||
if (isFilter) {
|
||||
addFilterPill(option, "include");
|
||||
search.value = "";
|
||||
clearHighlight();
|
||||
hidePanel();
|
||||
} else {
|
||||
selectOption(option);
|
||||
}
|
||||
} else if (key === "Escape") {
|
||||
clearHighlight();
|
||||
hidePanel();
|
||||
}
|
||||
} else if (key === "Escape") {
|
||||
clearHighlight();
|
||||
hidePanel();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user