v0.11.25 schedule list filter bug fixes

This commit is contained in:
2026-03-22 18:23:15 -04:00
parent 344ca70b64
commit 89bc8d00f7
4 changed files with 29 additions and 9 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "jama-backend", "name": "jama-backend",
"version": "0.11.24", "version": "0.11.25",
"description": "TeamChat backend server", "description": "TeamChat backend server",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {

View File

@@ -13,7 +13,7 @@
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
set -euo pipefail set -euo pipefail
VERSION="${1:-0.11.24}" VERSION="${1:-0.11.25}"
ACTION="${2:-}" ACTION="${2:-}"
REGISTRY="${REGISTRY:-}" REGISTRY="${REGISTRY:-}"
IMAGE_NAME="jama" IMAGE_NAME="jama"

View File

@@ -1,6 +1,6 @@
{ {
"name": "jama-frontend", "name": "jama-frontend",
"version": "0.11.24", "version": "0.11.25",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -261,7 +261,7 @@ function MiniCalendar({ selected, onChange, eventDates=new Set() }) {
} }
// ── Mobile Filter Bar (Schedule view: keyword+type filters with month nav; Day view: calendar accordion) ── // ── Mobile Filter Bar (Schedule view: keyword+type filters with month nav; Day view: calendar accordion) ──
function MobileScheduleFilter({ selected, onMonthChange, view, eventTypes, filterKeyword, onFilterKeyword, filterTypeId, onFilterTypeId, filterAvailability=false, onFilterAvailability, eventDates=new Set(), onInputFocus, onInputBlur }) { function MobileScheduleFilter({ selected, onMonthChange, view, eventTypes, filterKeyword, onFilterKeyword, filterTypeId, onFilterTypeId, filterAvailability=false, onFilterAvailability, onClearFromDate, eventDates=new Set(), onInputFocus, onInputBlur }) {
// Day view: keep accordion calendar // Day view: keep accordion calendar
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const y=selected.getFullYear(), m=selected.getMonth(); const y=selected.getFullYear(), m=selected.getMonth();
@@ -337,7 +337,7 @@ function MobileScheduleFilter({ selected, onMonthChange, view, eventTypes, filte
Requires Availability Requires Availability
</label> </label>
{hasFilters && ( {hasFilters && (
<button onClick={()=>{onFilterKeyword('');onFilterTypeId('');onFilterAvailability(false);}} style={{fontSize:12,color:'var(--error)',background:'none',border:'none',cursor:'pointer',padding:0}}> Clear all filters</button> <button onClick={()=>{onFilterKeyword('');onFilterTypeId('');onFilterAvailability(false);onClearFromDate?.();}} style={{fontSize:12,color:'var(--error)',background:'none',border:'none',cursor:'pointer',padding:0}}> Clear all filters</button>
)} )}
</div> </div>
)} )}
@@ -982,11 +982,21 @@ function expandEvents(events, rangeStart, rangeEnd) {
return result; return result;
} }
// Parse keyword string into match descriptors.
// Quoted terms ("mount") -> exact whole-word match only.
// Unquoted terms (mount) -> word-boundary prefix: term must start a word,
// so "mount" matches "mountain" but "mounte" does not.
function parseKeywords(raw) { function parseKeywords(raw) {
const terms = []; const terms = [];
const re = /"([^"]+)"|(\S+)/g; const re = /"([^"]+)"|(\S+)/g;
let match; let match;
while((match = re.exec(raw)) !== null) terms.push((match[1]||match[2]).toLowerCase()); while((match = re.exec(raw)) !== null) {
if (match[1] !== undefined) {
terms.push({ term: match[1].toLowerCase(), exact: true });
} else {
terms.push({ term: match[2].toLowerCase(), exact: false });
}
}
return terms; return terms;
} }
@@ -1028,7 +1038,16 @@ function ScheduleView({ events, selectedDate, onSelect, filterKeyword='', filter
if(filterAvailability && !e.track_availability) return false; if(filterAvailability && !e.track_availability) return false;
if(terms.length>0) { if(terms.length>0) {
const haystack=[e.title||'',e.location||'',e.description||''].join(' ').toLowerCase(); const haystack=[e.title||'',e.location||'',e.description||''].join(' ').toLowerCase();
if(!terms.some(t=>haystack.includes(t))) return false; const matches = ({ term, exact }) => {
if (exact) {
// Quoted: whole-word match only — term must be surrounded by word boundaries
return new RegExp('\\b' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b').test(haystack);
} else {
// Unquoted: prefix-of-word match — term must appear at the start of a word
return new RegExp('\\b' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).test(haystack);
}
};
if(!terms.some(matches)) return false;
} }
return true; return true;
}); });
@@ -1460,7 +1479,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
className="input" className="input"
placeholder={`Keyword… (space = OR, "phrase")`} placeholder={`Keyword… (space = OR, "phrase")`}
value={filterKeyword} value={filterKeyword}
onChange={e => setFilterKeyword(e.target.value)} autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} onChange={e => { setFilterKeyword(e.target.value); if (!e.target.value) setFilterFromDate(null); }} autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false}
style={{ marginBottom:8, fontSize:13 }} /> style={{ marginBottom:8, fontSize:13 }} />
<select <select
className="input" className="input"
@@ -1530,11 +1549,12 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
view={view} view={view}
eventTypes={eventTypes} eventTypes={eventTypes}
filterKeyword={filterKeyword} filterKeyword={filterKeyword}
onFilterKeyword={setFilterKeyword} onFilterKeyword={val => { setFilterKeyword(val); if (!val) setFilterFromDate(null); }}
filterTypeId={filterTypeId} filterTypeId={filterTypeId}
onFilterTypeId={setFilterTypeId} onFilterTypeId={setFilterTypeId}
filterAvailability={filterAvailability} filterAvailability={filterAvailability}
onFilterAvailability={setFilterAvailability} onFilterAvailability={setFilterAvailability}
onClearFromDate={() => setFilterFromDate(null)}
onInputFocus={()=>setInputFocused(true)} onInputFocus={()=>setInputFocused(true)}
onInputBlur={()=>setInputFocused(false)} onInputBlur={()=>setInputFocused(false)}
eventDates={eventDates} eventDates={eventDates}