fix(idaa): stabilize Recovery Meetings search and broaden visibility logic
- Resolved 400 Bad Request by whitelisting 'physical', 'virtual', and 'external_person_id' fields in backend searchable_fields. - Broadened server-side search by removing restrictive AND filters for location types, moving the inclusive OR logic to the client-side filter layer. - Hardened handle_search_refresh error handling to clear results on category filter changes (Type, Physical, Virtual) while maintaining flicker protection strictly for text search typing. - Restored permissive visibility for Trusted users, allowing them to see hidden/disabled items regardless of Edit Mode. - Optimized plural object loading in Archives and Events modules using concurrent Promise.all. - Standardized ID handling across the search flow to prevent Dexie lookup failures. - Finalized well-commented code across IDAA modules to document search strategies and workarounds.
This commit is contained in:
@@ -46,8 +46,12 @@
|
||||
let last_search_id = 0;
|
||||
|
||||
// Standardized Reactive Search Pattern (Aether UI V3)
|
||||
// This effect manages the orchestration between UI state and data fetching.
|
||||
$effect(() => {
|
||||
// 1. Reactive Dependencies
|
||||
const account_id = $ae_loc.account_id;
|
||||
if (!account_id) return; // Wait for account context
|
||||
|
||||
// Track filters and the search version (trigger)
|
||||
const qry_params = {
|
||||
v: $idaa_loc.recovery_meetings.search_version,
|
||||
@@ -57,8 +61,7 @@
|
||||
type: $idaa_loc.recovery_meetings.qry__type,
|
||||
limit: $idaa_loc.recovery_meetings.qry__limit,
|
||||
order: $idaa_loc.recovery_meetings.qry__order_by,
|
||||
remote: $idaa_loc.recovery_meetings.qry__remote_first,
|
||||
account: $ae_loc.account_id
|
||||
remote: $idaa_loc.recovery_meetings.qry__remote_first
|
||||
};
|
||||
|
||||
// 2. Debounce Logic
|
||||
@@ -75,21 +78,28 @@
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* SWR (Stale-While-Revalidate) Search Orchestrator
|
||||
*
|
||||
* GOAL: Render matching meetings in < 50ms, then update with perfect server data.
|
||||
*/
|
||||
async function handle_search_refresh() {
|
||||
const current_search_id = ++last_search_id;
|
||||
const account_id = $ae_loc.account_id;
|
||||
const remote_first = $idaa_loc.recovery_meetings.qry__remote_first;
|
||||
|
||||
if (!account_id) return;
|
||||
|
||||
if (log_lvl) console.log(`[Search #${current_search_id}] Refreshing recovery meetings (remote_first=${remote_first}) for account: ${account_id}...`);
|
||||
|
||||
$idaa_sess.recovery_meetings.qry__status = 'loading';
|
||||
|
||||
// If remote first, clear immediately to show fresh state
|
||||
// If 'Remote First' is toggled (Admin only), we clear results immediately to show fresh state.
|
||||
if (remote_first) {
|
||||
event_id_random_li = [];
|
||||
}
|
||||
|
||||
// Snapshot current params to ensure Fast Path matches revalidation
|
||||
// Snapshot parameters to ensure the Fast Path and Revalidation are using the same criteria.
|
||||
const qry_str = ($idaa_loc.recovery_meetings.qry__fulltext_str ?? '').toLowerCase().trim();
|
||||
const qry_physical = $idaa_loc.recovery_meetings.qry__physical;
|
||||
const qry_virtual = $idaa_loc.recovery_meetings.qry__virtual;
|
||||
@@ -97,55 +107,53 @@
|
||||
|
||||
let local_ids: string[] = [];
|
||||
|
||||
// 1. FAST PATH: Local IDB Search (SWR Pattern) - Skip if Remote First
|
||||
// 1. FAST PATH: Local IDB Search
|
||||
// We query Dexie first to show results from the 499-item cache pool instantly.
|
||||
if (!remote_first) {
|
||||
try {
|
||||
if (account_id) {
|
||||
let local_results = await db_events.event
|
||||
.filter(ev => {
|
||||
// Resilient account check: match either account_id or account_id_random
|
||||
const acct_match = ev.account_id === account_id || ev.account_id_random === account_id;
|
||||
if (!acct_match) return false;
|
||||
let local_results = await db_events.event
|
||||
.filter(ev => {
|
||||
// Resilient account check
|
||||
const acct_match = ev.account_id === account_id || ev.account_id_random === account_id;
|
||||
if (!acct_match) return false;
|
||||
|
||||
if (qry_type && ev.type !== qry_type) return false;
|
||||
if (qry_physical || qry_virtual) {
|
||||
let match = false;
|
||||
if (qry_physical && ev.physical) match = true;
|
||||
if (qry_virtual && ev.virtual) match = true;
|
||||
if (!match) return false;
|
||||
}
|
||||
if (qry_str) {
|
||||
const name = (ev.name ?? '').toLowerCase();
|
||||
const desc = (ev.description ?? '').toLowerCase();
|
||||
const loc = (ev.location_text ?? '').toLowerCase();
|
||||
return name.includes(qry_str) || desc.includes(qry_str) || loc.includes(qry_str);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.toArray();
|
||||
|
||||
// Sort local results
|
||||
if ($idaa_loc.recovery_meetings.qry__order_by === 'name') {
|
||||
local_results.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
|
||||
} else {
|
||||
local_results.sort((a, b) => {
|
||||
const dateA = a.updated_on ? new Date(a.updated_on).getTime() : 0;
|
||||
const dateB = b.updated_on ? new Date(b.updated_on).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
}
|
||||
|
||||
local_ids = local_results.map(e => e.id || e.event_id_random).filter(Boolean);
|
||||
|
||||
// Update UI immediately with local results
|
||||
if (current_search_id === last_search_id) {
|
||||
if (log_lvl) console.log(`[Search #${current_search_id}] Fast Path complete. Found ${local_ids.length} items locally.`);
|
||||
event_id_random_li = local_ids;
|
||||
// If we found results locally, we can mark as done to stop spinning,
|
||||
// revalidation will still finish in background
|
||||
if (local_ids.length > 0) {
|
||||
$idaa_sess.recovery_meetings.qry__status = 'done';
|
||||
if (qry_type && ev.type !== qry_type) return false;
|
||||
if (qry_physical || qry_virtual) {
|
||||
let match = false;
|
||||
// Loose equality to handle 1, '1', true from DB
|
||||
if (qry_physical && ev.physical == true) match = true;
|
||||
if (qry_virtual && ev.virtual == true) match = true;
|
||||
if (!match) return false;
|
||||
}
|
||||
if (qry_str) {
|
||||
const name = (ev.name ?? '').toLowerCase();
|
||||
const desc = (ev.description ?? '').toLowerCase();
|
||||
const loc = (ev.location_text ?? '').toLowerCase();
|
||||
return name.includes(qry_str) || desc.includes(qry_str) || loc.includes(qry_str);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.toArray();
|
||||
|
||||
// Sort local results matching UI selection
|
||||
if ($idaa_loc.recovery_meetings.qry__order_by === 'name') {
|
||||
local_results.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
|
||||
} else {
|
||||
local_results.sort((a, b) => {
|
||||
const dateA = a.updated_on ? new Date(a.updated_on).getTime() : 0;
|
||||
const dateB = b.updated_on ? new Date(b.updated_on).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
}
|
||||
|
||||
local_ids = local_results.map(e => String(e.id || e.event_id_random)).filter(Boolean);
|
||||
|
||||
// Update UI immediately. This eliminates the "white page" during searching.
|
||||
if (current_search_id === last_search_id) {
|
||||
if (log_lvl) console.log(`[Search #${current_search_id}] Fast Path complete. Found ${local_ids.length} items locally.`);
|
||||
event_id_random_li = local_ids;
|
||||
if (local_ids.length > 0) {
|
||||
$idaa_sess.recovery_meetings.qry__status = 'done';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -153,7 +161,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 2. REVALIDATE: Slow API Request
|
||||
// 2. REVALIDATE: Server API Search
|
||||
// This hits the full database, bypassing the "cache pool" limitation.
|
||||
try {
|
||||
const results = await events_func.search__event({
|
||||
api_cfg: $ae_api,
|
||||
@@ -172,18 +181,51 @@
|
||||
});
|
||||
|
||||
if (current_search_id === last_search_id) {
|
||||
const api_results = results || [];
|
||||
const api_ids = api_results.map((e: any) => e.id || e.event_id_random).filter(Boolean);
|
||||
let api_results = results || [];
|
||||
|
||||
// If API returns 0 but local search found broad results, protect the UI
|
||||
if (api_ids.length === 0 && local_ids.length > 0 && !remote_first && !qry_str) {
|
||||
if (log_lvl) console.warn(`[Search #${current_search_id}] Revalidation returned 0. Preserving cache.`);
|
||||
// SECONDARY FILTER: Ensure API results respect exact UI filters (handles backend broadness)
|
||||
api_results = api_results.filter((ev: any) => {
|
||||
if (qry_type && ev.type !== qry_type) return false;
|
||||
if (qry_physical || qry_virtual) {
|
||||
let match = false;
|
||||
if (qry_physical && ev.physical == true) match = true;
|
||||
if (qry_virtual && ev.virtual == true) match = true;
|
||||
if (!match) return false;
|
||||
}
|
||||
if (qry_str && qry_str.length >= 3) {
|
||||
const name = (ev.name ?? '').toLowerCase();
|
||||
const desc = (ev.description ?? '').toLowerCase();
|
||||
const loc = (ev.location_text ?? '').toLowerCase();
|
||||
if (!name.includes(qry_str) && !desc.includes(qry_str) && !loc.includes(qry_str)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const api_ids = api_results.map((e: any) => String(e.id || e.event_id_random)).filter(Boolean);
|
||||
|
||||
// UI PROTECTION: Preserve results if API returns nothing (0 results)
|
||||
// BUT only if it was a successful empty response, not a failure.
|
||||
// ALSO: Only protect if qry_str was the primary change.
|
||||
// If Type or Location changed, we want to see the 0.
|
||||
const filter_changed = qry_type !== $idaa_sess.recovery_meetings.status_qry__last_type ||
|
||||
qry_physical !== $idaa_sess.recovery_meetings.status_qry__last_phys ||
|
||||
qry_virtual !== $idaa_sess.recovery_meetings.status_qry__last_virt;
|
||||
|
||||
if (api_ids.length === 0 && local_ids.length > 0 && !remote_first && !filter_changed) {
|
||||
if (log_lvl) console.warn(`[Search #${current_search_id}] API returned 0 matching results. Preserving local cache view.`);
|
||||
$idaa_sess.recovery_meetings.qry__status = 'done';
|
||||
return;
|
||||
}
|
||||
|
||||
$idaa_slct.event_obj_li = api_results;
|
||||
event_id_random_li = api_ids;
|
||||
|
||||
// Snapshot last used filters for future protection checks
|
||||
$idaa_sess.recovery_meetings.status_qry__last_request_str = qry_str;
|
||||
$idaa_sess.recovery_meetings.status_qry__last_type = qry_type;
|
||||
$idaa_sess.recovery_meetings.status_qry__last_phys = qry_physical;
|
||||
$idaa_sess.recovery_meetings.status_qry__last_virt = qry_virtual;
|
||||
|
||||
$idaa_sess.recovery_meetings.qry__status = 'done';
|
||||
if (log_lvl) console.log(`[Search #${current_search_id}] Revalidation Complete. Found ${api_ids.length} items.`);
|
||||
}
|
||||
@@ -191,9 +233,11 @@
|
||||
if (current_search_id === last_search_id) {
|
||||
console.error('Revalidation failed:', error);
|
||||
$idaa_sess.recovery_meetings.qry__status = 'error';
|
||||
if (event_id_random_li.length === 0 && local_ids.length > 0) {
|
||||
event_id_random_li = local_ids;
|
||||
}
|
||||
|
||||
// If the API failed (e.g. 400/500), we should NOT preserve results from a
|
||||
// previous successful search as it's misleading.
|
||||
// We clear the list so the 'No recovery meetings found' / Error message shows.
|
||||
event_id_random_li = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user