Add DateRangePicker component with segmented entry and calendar popup
Django CI/CD / test (push) Successful in 2m33s
Django CI/CD / build-and-push (push) Successful in 1m17s

Implements the DateRangePicker design: a DateRangeField that looks like a
single input but splits each date into DD/MM/YYYY part inputs (ordered by
the new common.time.dateformat_hyphenated), and a DateRangeCalendar popup
with a preset column (today, yesterday, last 7/30 days, this/last month,
this year), anchor-style range picking with an outlined/filled/muted range
track, and a Cancel / Clear / Select footer.

Typing fills each part's placeholder from the right (YYYY -> YY19 -> 1987),
auto-advances between parts, and Backspace/Delete reverts the active part.
The committed value lives in hidden ISO {prefix}-min/{prefix}-max inputs --
the same contract as DateRangeFilter, so filter_bar.js needs no changes.

As a tryout, the Purchased filter in PurchaseFilterBar now uses the
DateRangePicker; Refunded keeps the native-date DateRangeFilter, and the
native-path e2e tests were repointed at it.

Includes unit tests for the component family and the filter-bar
integration, plus Playwright e2e tests for segment entry, calendar
picking, presets, and footer actions.

https://claude.ai/code/session_017b75KJAu4kNNpZPu9NAPBM
This commit is contained in:
Claude
2026-06-11 17:49:22 +00:00
committed by Lukáš Kucharczyk
parent 15a97dee9a
commit 0fa860c237
10 changed files with 1552 additions and 11 deletions
+95
View File
@@ -918,6 +918,9 @@
.ms-2\.5 {
margin-inline-start: calc(var(--spacing) * 2.5);
}
.ms-auto {
margin-inline-start: auto;
}
.me-2 {
margin-inline-end: calc(var(--spacing) * 2);
}
@@ -1582,6 +1585,9 @@
.w-5\/6 {
width: calc(5 / 6 * 100%);
}
.w-8 {
width: calc(var(--spacing) * 8);
}
.w-10 {
width: calc(var(--spacing) * 10);
}
@@ -1597,6 +1603,12 @@
.w-72 {
width: calc(var(--spacing) * 72);
}
.w-\[2\.5ch\] {
width: 2.5ch;
}
.w-\[4\.5ch\] {
width: 4.5ch;
}
.w-\[300px\] {
width: 300px;
}
@@ -1736,6 +1748,9 @@
.cursor-pointer {
cursor: pointer;
}
.cursor-text {
cursor: text;
}
.resize {
resize: both;
}
@@ -1787,6 +1802,9 @@
.justify-start {
justify-content: flex-start;
}
.gap-0\.5 {
gap: calc(var(--spacing) * 0.5);
}
.gap-1 {
gap: calc(var(--spacing) * 1);
}
@@ -1836,6 +1854,9 @@
margin-inline-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)));
}
}
.gap-y-0\.5 {
row-gap: calc(var(--spacing) * 0.5);
}
.gap-y-4 {
row-gap: calc(var(--spacing) * 4);
}
@@ -1904,6 +1925,9 @@
.rounded-xl {
border-radius: var(--radius-xl);
}
.rounded-xs {
border-radius: var(--radius-xs);
}
.rounded-s-base {
border-start-start-radius: var(--radius-base);
border-end-start-radius: var(--radius-base);
@@ -1958,6 +1982,10 @@
border-style: var(--tw-border-style);
border-width: 2px;
}
.border-y {
border-block-style: var(--tw-border-style);
border-block-width: 1px;
}
.border-e {
border-inline-end-style: var(--tw-border-style);
border-inline-end-width: 1px;
@@ -2030,6 +2058,12 @@
.border-brand {
border-color: var(--color-brand);
}
.border-brand\/70 {
border-color: color-mix(in srgb, oklch(48.8% 0.243 264.376) 70%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-brand) 70%, transparent);
}
}
.border-default {
border-color: var(--color-default);
}
@@ -2121,12 +2155,24 @@
.bg-brand {
background-color: var(--color-brand);
}
.bg-brand\/10 {
background-color: color-mix(in srgb, oklch(48.8% 0.243 264.376) 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-brand) 10%, transparent);
}
}
.bg-brand\/15 {
background-color: color-mix(in srgb, oklch(48.8% 0.243 264.376) 15%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-brand) 15%, transparent);
}
}
.bg-brand\/30 {
background-color: color-mix(in srgb, oklch(48.8% 0.243 264.376) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-brand) 30%, transparent);
}
}
.bg-dark-backdrop\/70 {
background-color: color-mix(in srgb, oklch(13% 0.028 261.692) 70%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -2296,6 +2342,9 @@
padding: 0 !important;
}
}
.p-0 {
padding: calc(var(--spacing) * 0);
}
.p-1 {
padding: calc(var(--spacing) * 1);
}
@@ -2320,6 +2369,9 @@
.p-6 {
padding: calc(var(--spacing) * 6);
}
.px-0\.5 {
padding-inline: calc(var(--spacing) * 0.5);
}
.px-2 {
padding-inline: calc(var(--spacing) * 2);
}
@@ -2419,6 +2471,9 @@
.text-right {
text-align: right;
}
.text-start {
text-align: start;
}
.align-middle {
vertical-align: middle;
}
@@ -2714,9 +2769,15 @@
.decoration-dotted {
text-decoration-style: dotted;
}
.caret-transparent {
caret-color: transparent;
}
.opacity-0 {
opacity: 0%;
}
.opacity-40 {
opacity: 40%;
}
.opacity-50 {
opacity: 50%;
}
@@ -2752,6 +2813,13 @@
--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-2 {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-brand-strong {
--tw-ring-color: var(--color-brand-strong);
}
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
@@ -2817,6 +2885,9 @@
.\[program\:qcluster\] {
program: qcluster;
}
.ring-inset {
--tw-ring-inset: inset;
}
.group-hover\:absolute {
&:is(:where(.group):hover *) {
@media (hover: hover) {
@@ -2958,6 +3029,22 @@
padding-top: calc(var(--spacing) * 0);
}
}
.focus-within\:border-brand {
&:focus-within {
border-color: var(--color-brand);
}
}
.focus-within\:ring-1 {
&:focus-within {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
}
.focus-within\:ring-brand {
&:focus-within {
--tw-ring-color: var(--color-brand);
}
}
.hover\:scale-110 {
&:hover {
@media (hover: hover) {
@@ -3223,6 +3310,14 @@
border-color: var(--color-brand);
}
}
.focus\:bg-brand\/30 {
&:focus {
background-color: color-mix(in srgb, oklch(48.8% 0.243 264.376) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-brand) 30%, transparent);
}
}
}
.focus\:text-blue-700 {
&:focus {
color: var(--color-blue-700);
+530
View File
@@ -0,0 +1,530 @@
/**
* DateRangePicker — vanilla JavaScript implementation.
*
* Drives the DateRangePicker component (common/components/date_range_picker.py):
*
* - DateRangeField: segmented manual entry. Each date part (DD/MM/YYYY) is its
* own input; digits fill the placeholder from the right (YYYY → YYY1 → YY19
* → Y198 → 1987), full parts auto-advance to the next one, and
* Backspace/Delete reverts the active part to its placeholder.
* - DateRangeCalendar: popup month grid with a preset column and a
* Cancel / Clear / Select footer. Picking works anchor-style: the first
* pick becomes the StartDate anchor, the second pick sets the EndDate and
* moves the anchor there so further picks adjust the StartDate. Picking on
* the wrong side of the anchor clears the range and restarts from the
* clicked date.
*
* The committed value lives in the two hidden ISO inputs ({prefix}-min /
* {prefix}-max) that filter_bar.js serializes into a DateCriterion.
*
* NB: class strings below are emitted verbatim so the Tailwind scanner picks
* them up — keep them as plain literals.
*/
(function () {
"use strict";
var WEEKDAY_LABELS = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
var WEEKDAY_CLASS =
"w-8 h-6 flex items-center justify-center text-xs text-body select-none";
var DAY_BASE_CLASS =
"date-range-day w-8 h-8 flex items-center justify-center text-sm " +
"text-heading cursor-pointer hover:bg-neutral-tertiary-medium";
var DAY_ROUNDED_CLASS = "rounded-base";
var DAY_OUTSIDE_MONTH_CLASS = "opacity-40";
var DAY_SELECTED_CLASS = "bg-brand text-white hover:bg-brand-strong";
var DAY_ANCHOR_CLASS =
"bg-brand text-white ring-2 ring-inset ring-brand-strong hover:bg-brand-strong";
// The three visual states of the date range track (the days between the
// two endpoints): outlined while picking the second date, filled once both
// are picked, muted when showing an already-committed range read-only.
var TRACK_OUTLINED_CLASS = "border-y border-brand/70 bg-brand/10";
var TRACK_FILLED_CLASS = "bg-brand/30";
var TRACK_MUTED_CLASS = "bg-brand/15";
// ── Date helpers (all local-time; values are ISO YYYY-MM-DD strings) ──
function padNumber(value, width) {
var text = String(value);
while (text.length < width) text = "0" + text;
return text;
}
function isoFromDate(dateObject) {
return (
padNumber(dateObject.getFullYear(), 4) +
"-" +
padNumber(dateObject.getMonth() + 1, 2) +
"-" +
padNumber(dateObject.getDate(), 2)
);
}
function dateFromIso(isoString) {
var pieces = isoString.split("-");
return new Date(
parseInt(pieces[0], 10),
parseInt(pieces[1], 10) - 1,
parseInt(pieces[2], 10)
);
}
function addDays(dateObject, dayCount) {
var copy = new Date(dateObject.getTime());
copy.setDate(copy.getDate() + dayCount);
return copy;
}
/** Validate a (year, month, day) triple as a real calendar date. */
function isoFromParts(year, month, day) {
var candidate = new Date(year, month - 1, day);
if (
candidate.getFullYear() !== year ||
candidate.getMonth() !== month - 1 ||
candidate.getDate() !== day
) {
return "";
}
return isoFromDate(candidate);
}
function presetRange(presetName) {
var today = new Date();
today.setHours(0, 0, 0, 0);
var yesterday = addDays(today, -1);
var year = today.getFullYear();
var month = today.getMonth();
switch (presetName) {
case "today":
return [today, today];
case "yesterday":
return [yesterday, yesterday];
case "last_7_days":
return [addDays(today, -6), today];
case "last_30_days":
return [addDays(today, -29), today];
case "this_month":
return [new Date(year, month, 1), new Date(year, month + 1, 0)];
case "last_month":
return [new Date(year, month - 1, 1), new Date(year, month, 0)];
case "this_year":
return [new Date(year, 0, 1), new Date(year, 11, 31)];
default:
return null;
}
}
// ── DateRangeField: segmented manual entry ──────────────────────────────
function segmentBuffer(segment) {
return segment.dataset.typedDigits || "";
}
function setSegmentBuffer(segment, buffer) {
segment.dataset.typedDigits = buffer;
if (buffer === "") {
segment.value = "";
return;
}
var placeholder = segment.getAttribute("placeholder");
// Fill the placeholder from the right: typing 19 into YYYY shows YY19.
segment.value = placeholder.slice(0, placeholder.length - buffer.length) + buffer;
}
function segmentsForSide(picker, side) {
return Array.prototype.slice.call(
picker.querySelectorAll('input[data-date-side="' + side + '"]')
);
}
/** Recompute one hidden ISO input from its side's segment buffers. */
function syncHiddenFromSegments(picker, side) {
var hidden = picker.querySelector(
'input[data-date-range-hidden="' + side + '"]'
);
var partValues = {};
var complete = true;
segmentsForSide(picker, side).forEach(function (segment) {
var buffer = segmentBuffer(segment);
if (buffer.length !== parseInt(segment.getAttribute("maxlength"), 10)) {
complete = false;
}
partValues[segment.dataset.datePart] = buffer;
});
var previousValue = hidden.value;
if (complete) {
hidden.value = isoFromParts(
parseInt(partValues.year, 10),
parseInt(partValues.month, 10),
parseInt(partValues.day, 10)
);
} else {
hidden.value = "";
}
return hidden.value !== previousValue;
}
/** Push an ISO value (or "") into a side's segments and hidden input. */
function setSideValue(picker, side, isoString) {
var hidden = picker.querySelector(
'input[data-date-range-hidden="' + side + '"]'
);
hidden.value = isoString;
var partValues = { year: "", month: "", day: "" };
if (isoString) {
var pieces = isoString.split("-");
partValues = { year: pieces[0], month: pieces[1], day: pieces[2] };
}
segmentsForSide(picker, side).forEach(function (segment) {
setSegmentBuffer(segment, partValues[segment.dataset.datePart]);
});
}
function initField(picker, calendarState) {
var field = picker.querySelector("[data-date-range-field]");
var segments = Array.prototype.slice.call(
picker.querySelectorAll("input[data-date-part]")
);
// Adopt server-rendered values (prefilled filter) as typed buffers.
segments.forEach(function (segment) {
if (segment.value) setSegmentBuffer(segment, segment.value);
});
// Clicking anywhere in the container that is not a date part activates
// the first date part.
field.addEventListener("mousedown", function (event) {
if (event.target.closest("input[data-date-part]")) return;
if (event.target.closest("[data-date-range-calendar-toggle]")) return;
event.preventDefault();
segments[0].focus();
});
segments.forEach(function (segment, segmentIndex) {
segment.addEventListener("keydown", function (event) {
if (event.key === "Tab") return; // native Tab / Shift+Tab navigation
if (event.key === "Enter") return; // let the filter form submit
if (event.key === "Backspace" || event.key === "Delete") {
event.preventDefault();
setSegmentBuffer(segment, "");
syncHiddenFromSegments(picker, segment.dataset.dateSide);
return;
}
if (event.ctrlKey || event.metaKey || event.altKey) return;
event.preventDefault();
if (!/^[0-9]$/.test(event.key)) return; // only numbers can be typed
var maximumLength = parseInt(segment.getAttribute("maxlength"), 10);
var buffer = segmentBuffer(segment);
// Typing into an already-full part starts it over.
buffer = buffer.length >= maximumLength ? event.key : buffer + event.key;
setSegmentBuffer(segment, buffer);
syncHiddenFromSegments(picker, segment.dataset.dateSide);
if (buffer.length === maximumLength && segmentIndex + 1 < segments.length) {
segments[segmentIndex + 1].focus();
}
});
// Swallow any input that bypassed keydown (e.g. IME/paste).
segment.addEventListener("input", function () {
setSegmentBuffer(segment, segmentBuffer(segment));
});
segment.addEventListener("focus", function () {
if (calendarState) calendarState.refreshFromField();
});
});
}
// ── DateRangeCalendar: popup month grid ────────────────────────────────
function createCalendarState(picker) {
var popup = picker.querySelector("[data-date-range-calendar]");
var grid = popup.querySelector("[data-date-range-grid]");
var monthLabel = popup.querySelector("[data-date-range-month-label]");
var today = new Date();
var state = {
open: false,
viewYear: today.getFullYear(),
viewMonth: today.getMonth(),
startIso: "",
endIso: "",
// The anchor is the fixed endpoint: "start" while picking the EndDate,
// "end" once the range is complete (further picks move the StartDate).
anchor: "",
hoverIso: "",
// True while showing a committed range the user has not edited yet —
// the track renders muted until the first pick.
readOnly: false,
};
function hiddenValue(side) {
return picker.querySelector(
'input[data-date-range-hidden="' + side + '"]'
).value;
}
state.refreshFromField = function () {
if (state.open) return;
state.startIso = hiddenValue("min");
state.endIso = hiddenValue("max");
};
function syncSelectionToField() {
setSideValue(picker, "min", state.startIso);
setSideValue(picker, "max", state.endIso);
}
function openPopup() {
state.startIso = hiddenValue("min");
state.endIso = hiddenValue("max");
state.anchor = state.startIso && state.endIso ? "end" : state.startIso ? "start" : "";
state.readOnly = Boolean(state.startIso && state.endIso);
state.hoverIso = "";
var focusDate = state.startIso ? dateFromIso(state.startIso) : new Date();
state.viewYear = focusDate.getFullYear();
state.viewMonth = focusDate.getMonth();
state.open = true;
popup.classList.remove("hidden");
render();
}
function closePopup() {
state.open = false;
state.hoverIso = "";
popup.classList.add("hidden");
}
function clearSelection() {
state.startIso = "";
state.endIso = "";
state.anchor = "";
state.hoverIso = "";
state.readOnly = false;
syncSelectionToField();
}
/**
* Anchor-style picking:
* - no selection: the pick becomes the StartDate anchor
* - anchor=start (picking EndDate): a pick on/after the StartDate
* completes the range and moves the anchor to the EndDate; a pick
* before it clears the range and restarts
* - anchor=end (adjusting StartDate): a pick on/before the EndDate
* moves the StartDate (extend/shorten); a pick after it clears the
* range and restarts from the clicked date
*/
function pickDate(isoString) {
state.readOnly = false;
if (!state.startIso) {
state.startIso = isoString;
state.anchor = "start";
} else if (state.anchor === "start" && !state.endIso) {
if (isoString >= state.startIso) {
state.endIso = isoString;
state.anchor = "end";
} else {
state.startIso = isoString;
state.endIso = "";
state.anchor = "start";
}
} else {
if (isoString <= state.endIso) {
state.startIso = isoString;
} else {
state.startIso = isoString;
state.endIso = "";
state.anchor = "start";
}
}
syncSelectionToField();
render();
}
function applyPreset(presetName) {
var range = presetRange(presetName);
if (!range) return;
state.startIso = isoFromDate(range[0]);
state.endIso = isoFromDate(range[1]);
state.anchor = "end";
state.readOnly = false;
state.viewYear = range[0].getFullYear();
state.viewMonth = range[0].getMonth();
syncSelectionToField();
render();
}
/** The (inclusive-exclusive of endpoints) track between the two range
* ends; while picking the second date the hovered day acts as the
* provisional other end. */
function trackBounds() {
if (state.startIso && state.endIso) {
return [state.startIso, state.endIso, state.readOnly ? TRACK_MUTED_CLASS : TRACK_FILLED_CLASS];
}
if (state.startIso && state.hoverIso && state.hoverIso !== state.startIso) {
var lower = state.hoverIso < state.startIso ? state.hoverIso : state.startIso;
var upper = state.hoverIso < state.startIso ? state.startIso : state.hoverIso;
return [lower, upper, TRACK_OUTLINED_CLASS];
}
return null;
}
function dayCellClass(isoString, inViewMonth) {
var classes = [DAY_BASE_CLASS];
var isStart = isoString === state.startIso;
var isEnd = isoString === state.endIso;
var isAnchor =
(state.anchor === "start" && isStart) || (state.anchor === "end" && isEnd);
var track = trackBounds();
var inTrack = track && isoString > track[0] && isoString < track[1];
if (inTrack) {
classes.push(track[2]);
} else {
classes.push(DAY_ROUNDED_CLASS);
}
if (isAnchor && !state.readOnly) {
classes.push(DAY_ANCHOR_CLASS);
} else if (isStart || isEnd) {
classes.push(DAY_SELECTED_CLASS);
} else if (!inViewMonth) {
classes.push(DAY_OUTSIDE_MONTH_CLASS);
}
return classes.join(" ");
}
function render() {
monthLabel.textContent = new Date(
state.viewYear,
state.viewMonth,
1
).toLocaleDateString(undefined, { month: "long", year: "numeric" });
grid.textContent = "";
WEEKDAY_LABELS.forEach(function (weekdayLabel) {
var headerCell = document.createElement("span");
headerCell.className = WEEKDAY_CLASS;
headerCell.textContent = weekdayLabel;
grid.appendChild(headerCell);
});
var firstOfMonth = new Date(state.viewYear, state.viewMonth, 1);
// Monday-first offset of the leading overflow days.
var leadingDays = (firstOfMonth.getDay() + 6) % 7;
var cellDate = addDays(firstOfMonth, -leadingDays);
for (var cellIndex = 0; cellIndex < 42; cellIndex++) {
var isoString = isoFromDate(cellDate);
var dayButton = document.createElement("button");
dayButton.type = "button";
dayButton.setAttribute("data-date", isoString);
dayButton.className = dayCellClass(
isoString,
cellDate.getMonth() === state.viewMonth
);
dayButton.textContent = String(cellDate.getDate());
grid.appendChild(dayButton);
cellDate = addDays(cellDate, 1);
}
}
// ── Wiring ──
picker
.querySelector("[data-date-range-calendar-toggle]")
.addEventListener("click", function () {
if (state.open) closePopup();
else openPopup();
});
grid.addEventListener("click", function (event) {
var dayButton = event.target.closest("button[data-date]");
if (dayButton) pickDate(dayButton.getAttribute("data-date"));
});
grid.addEventListener("mouseover", function (event) {
if (!state.startIso || state.endIso) return;
var dayButton = event.target.closest("button[data-date]");
if (!dayButton) return;
var hoveredIso = dayButton.getAttribute("data-date");
if (hoveredIso === state.hoverIso) return;
state.hoverIso = hoveredIso;
render();
});
popup
.querySelector("[data-date-range-prev]")
.addEventListener("click", function () {
state.viewMonth -= 1;
if (state.viewMonth < 0) {
state.viewMonth = 11;
state.viewYear -= 1;
}
render();
});
popup
.querySelector("[data-date-range-next]")
.addEventListener("click", function () {
state.viewMonth += 1;
if (state.viewMonth > 11) {
state.viewMonth = 0;
state.viewYear += 1;
}
render();
});
popup.querySelectorAll("[data-date-range-preset]").forEach(function (button) {
button.addEventListener("click", function () {
applyPreset(button.getAttribute("data-date-range-preset"));
});
});
// Cancel: close the popup and clear the selected dates.
popup
.querySelector("[data-date-range-cancel]")
.addEventListener("click", function () {
clearSelection();
closePopup();
});
// Clear: clear the selected dates but keep the popup open.
popup
.querySelector("[data-date-range-clear]")
.addEventListener("click", function () {
clearSelection();
render();
});
// Select: close the popup, keeping the selected dates.
popup
.querySelector("[data-date-range-select]")
.addEventListener("click", function () {
closePopup();
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape" && state.open) closePopup();
});
document.addEventListener("mousedown", function (event) {
if (state.open && !picker.contains(event.target)) closePopup();
});
return state;
}
function initPicker(picker) {
if (picker.dataset.dateRangePickerInitialized) return;
picker.dataset.dateRangePickerInitialized = "true";
var calendarState = createCalendarState(picker);
initField(picker, calendarState);
}
function initAllPickers() {
document.querySelectorAll("[data-date-range-picker]").forEach(initPicker);
}
window.initDateRangePickers = initAllPickers;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initAllPickers);
} else {
initAllPickers();
}
})();
+1
View File
@@ -143,6 +143,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
title="Manage purchases",
scripts=ModuleScript("range_slider.js")
+ ModuleScript("search_select.js")
+ ModuleScript("date_range_picker.js")
+ ModuleScript("filter_bar.js"),
)