Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions frontend/src/_util/document-search-autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,72 @@ function applySuggestion(searchText, cursorPos, suggestion) {
return null;
}

/**
* When the cursor is inside a Date(…) or new Date(…) argument, returns the
* slice indices of that argument so a picker can replace it with a quoted ISO string.
*/
function getDatePickerInsertionRange(searchText, cursorPos) {
const before = searchText.slice(0, cursorPos);
const re = /((?:new\s+)?Date\s*\(\s*)([^)]*)$/i;
const m = before.match(re);
if (!m) {
return null;
}
const innerStart = m.index + m[1].length;
const after = searchText.slice(cursorPos);
let closeIdx = -1;
let parenDepth = 0;
for (let k = 0; k < after.length; k++) {
const ch = after[k];
if (ch === '(') {
parenDepth++;
} else if (ch === ')') {
if (parenDepth === 0) {
closeIdx = cursorPos + k;
break;
}
parenDepth--;
}
}
const innerEnd = closeIdx >= 0 ? closeIdx : cursorPos;
Comment thread
IslandRhythms marked this conversation as resolved.
const needsClosingParen = closeIdx < 0;
return { innerStart, innerEnd, needsClosingParen };
}

function dateArgumentSliceToDatetimeLocal(slice) {
const t = slice.trim();
if (!t) {
return '';
}
const unquoted = (t.startsWith('"') && t.endsWith('"')) || (t.startsWith('\'') && t.endsWith('\''))
? t.slice(1, -1)
: t;
const d = new Date(unquoted);
if (Number.isNaN(d.getTime())) {
return '';
}
const pad = n => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}

function insertQuotedIsoInDateArgument(searchText, range, isoString) {
const quoted = JSON.stringify(isoString);
const { innerStart, innerEnd } = range;
const closing = range.needsClosingParen === true ? ')' : '';
return {
text: searchText.slice(0, innerStart) + quoted + closing + searchText.slice(innerEnd),
newCursorPos: innerStart + quoted.length + closing.length
};
}

module.exports = {
buildAutocompleteTrie,
getAutocompleteContext,
getAutocompleteSuggestions,
applySuggestion,
getDatePickerInsertionRange,
dateArgumentSliceToDatetimeLocal,
insertQuotedIsoInDateArgument,
Comment thread
IslandRhythms marked this conversation as resolved.
QUERY_SELECTORS,
VALUE_HELPERS,
FUNCTION_HELPERS
Expand Down
205 changes: 205 additions & 0 deletions frontend/src/models/date-range-filter/date-range-filter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<div class="relative shrink-0" ref="root">
<button
type="button"
@click="togglePanel"
:class="[
'rounded-md p-1.5 shrink-0 border shadow-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary',
panelOpen
? 'border-primary bg-primary-subtle text-primary ring-1 ring-inset ring-primary/25'
: 'border-edge-strong bg-surface text-content-secondary hover:bg-muted hover:text-content hover:border-edge'
]"
title="Filter by date"
aria-label="Filter by date"
:aria-expanded="panelOpen"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" />
</svg>
</button>
<div
v-if="panelOpen"
class="absolute right-0 left-auto top-full z-[80] mt-1 w-[min(17.5rem,calc(100vw-0.75rem))] max-h-[min(26rem,calc(100dvh-4.5rem))] overflow-x-hidden overflow-y-auto overscroll-contain rounded-md border border-edge bg-surface py-2 px-2.5 shadow-lg ring-1 ring-black/5"
role="dialog"
aria-label="Date filter"
@keyup.esc.prevent="closePanel"
>
<div class="mb-1.5 flex flex-wrap items-center justify-between gap-1.5">
<div class="text-[10px] font-semibold uppercase tracking-wider leading-tight text-gray-400">Anchor (UTC)</div>
<div
class="isolate inline-flex shrink-0 rounded-md shadow-sm ring-1 ring-inset ring-edge"
role="group"
aria-label="Anchor input mode"
>
<button
type="button"
@click="anchorIsoMode = false"
class="relative inline-flex items-center rounded-none rounded-l-md px-2 py-0.5 text-[10px] font-semibold focus:z-10 focus:outline-none focus:ring-1 focus:ring-primary-subtle"
:class="!anchorIsoMode ? 'bg-primary-subtle text-primary z-[1]' : 'bg-page text-content-secondary hover:bg-muted'"
>
Numbers
</button>
<button
type="button"
@click="anchorIsoMode = true"
class="relative -ml-px inline-flex items-center rounded-none rounded-r-md px-2 py-0.5 text-[10px] font-semibold focus:z-10 focus:outline-none focus:ring-1 focus:ring-primary-subtle"
:class="anchorIsoMode ? 'bg-primary-subtle text-primary z-[1]' : 'bg-page text-content-secondary hover:bg-muted'"
>
ISO
</button>
</div>
</div>
<div v-if="anchorIsoMode" class="mb-2">
<label class="block">
<span class="sr-only">ISO 8601</span>
<input
v-model="isoAnchorInput"
type="text"
autocomplete="off"
spellcheck="false"
placeholder="2026-05-08T14:30:00Z"
class="w-full rounded border border-edge-strong px-1.5 py-1 font-mono text-xs text-content placeholder:text-gray-400 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary-subtle"
@paste="onIsoAnchorPaste"
/>
</label>
</div>
<div v-else class="mb-2 grid grid-cols-3 gap-x-1.5 gap-y-1.5">
<label class="flex min-w-0 flex-col gap-0.5">
<span class="text-[10px] font-medium leading-none text-content-secondary">Year</span>
<input
v-model.number="anchorYear"
type="number"
inputmode="numeric"
min="1970"
max="2100"
step="1"
placeholder="–"
class="w-full min-w-0 rounded border border-edge-strong px-1.5 py-1 text-xs tabular-nums leading-snug text-content focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary-subtle"
/>
</label>
<label class="flex min-w-0 flex-col gap-0.5">
<span class="text-[10px] font-medium leading-none text-content-secondary">Mo</span>
<input
v-model.number="anchorMonth"
type="number"
inputmode="numeric"
min="1"
max="12"
step="1"
placeholder="–"
class="w-full min-w-0 rounded border border-edge-strong px-1.5 py-1 text-xs tabular-nums leading-snug text-content focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary-subtle"
/>
</label>
<label class="flex min-w-0 flex-col gap-0.5">
<span class="text-[10px] font-medium leading-none text-content-secondary">Day</span>
<input
v-model.number="anchorDay"
type="number"
inputmode="numeric"
min="1"
max="31"
step="1"
placeholder="–"
class="w-full min-w-0 rounded border border-edge-strong px-1.5 py-1 text-xs tabular-nums leading-snug text-content focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary-subtle"
/>
</label>
<label class="flex min-w-0 flex-col gap-0.5">
<span class="text-[10px] font-medium leading-none text-content-secondary">Hr</span>
<input
v-model.number="anchorHour"
type="number"
inputmode="numeric"
min="0"
max="23"
step="1"
placeholder="–"
class="w-full min-w-0 rounded border border-edge-strong px-1.5 py-1 text-xs tabular-nums leading-snug text-content focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary-subtle"
/>
</label>
<label class="flex min-w-0 flex-col gap-0.5">
<span class="text-[10px] font-medium leading-none text-content-secondary">Min</span>
<input
v-model.number="anchorMinute"
type="number"
inputmode="numeric"
min="0"
max="59"
step="1"
placeholder="–"
class="w-full min-w-0 rounded border border-edge-strong px-1.5 py-1 text-xs tabular-nums leading-snug text-content focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary-subtle"
/>
</label>
</div>
<div v-if="!useCustomPath" class="mb-2">
<label class="block text-[10px] font-semibold uppercase tracking-wider text-gray-400 mb-0.5">Date field</label>
<select
v-model="selectedPath"
class="w-full max-w-full rounded border border-edge-strong px-1.5 py-1 text-xs text-content focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary-subtle"
>
<option v-for="p in datePaths" :key="p" :value="p">{{ p }}</option>
</select>
</div>
<div v-else class="mb-2">
<label class="block text-[10px] font-semibold uppercase tracking-wider text-gray-400 mb-0.5">Field path</label>
<input
v-model="customPath"
type="text"
placeholder="createdAt"
class="w-full max-w-full rounded border border-edge-strong px-1.5 py-1 text-xs font-mono text-content placeholder:text-gray-400 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary-subtle"
/>
<p class="mt-0.5 text-[10px] leading-snug text-content-tertiary">No Date fields in schema; enter path.</p>
</div>
<div class="mb-2 grid grid-cols-2 gap-1">
<button
type="button"
title="Replace filter: field strictly greater than UTC anchor (hour and minute included)"
class="rounded bg-primary px-2 py-1.5 text-xs font-semibold tabular-nums text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
@click="applyAnchorComparison('$gt')"
>
$gt
</button>
<button
type="button"
title="Replace filter: field strictly less than UTC anchor (hour and minute included)"
class="rounded bg-primary px-2 py-1.5 text-xs font-semibold tabular-nums text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
@click="applyAnchorComparison('$lt')"
>
$lt
</button>
</div>
<div class="text-[10px] font-semibold uppercase tracking-wider text-gray-400 mb-0.5">From anchor instant</div>
<div class="grid grid-cols-2 gap-1">
<button
type="button"
title="Replace filter—7 days ending at anchor instant"
class="rounded border border-edge bg-page px-1.5 py-1 text-left text-[10px] leading-tight text-content-secondary hover:bg-muted"
@click="presetRangePreviousDays(7)"
>
−7 d
</button>
<button
type="button"
title="Replace filter—7 days starting at anchor instant"
class="rounded border border-edge bg-page px-1.5 py-1 text-left text-[10px] leading-tight text-content-secondary hover:bg-muted"
@click="presetRangeNextDays(7)"
>
+7 d
</button>
<button
type="button"
title="Replace filter—3 weeks ending at anchor instant"
class="rounded border border-edge bg-page px-1.5 py-1 text-left text-[10px] leading-tight text-content-secondary hover:bg-muted"
@click="presetRangePreviousWeeks(3)"
>
−3 wk
</button>
<button
type="button"
title="Replace filter—3 weeks starting at anchor instant"
class="rounded border border-edge bg-page px-1.5 py-1 text-left text-[10px] leading-tight text-content-secondary hover:bg-muted"
@click="presetRangeNextWeeks(3)"
>
+3 wk
</button>
</div>
</div>
</div>
Loading
Loading