fix(cases): UAT Section 4 — server-side search, context-aware empty state, doc rewrite (#27)#42
Conversation
…4 doc rewrite (#27) Issue #27 — UAT findings for Section 4 of the Cases Worklist. Backend - `GET /api/cases` accepts an optional `search` query param. - `CaseFlowService.listCases` adds a case-insensitive ILIKE filter on `employees.name` and `employees.external_id` so search spans the full worklist instead of being limited to the first 25-page client filter. Frontend (`/cases`) - Search input is debounced (300 ms), syncs to `?search=` in the URL, and is forwarded to the backend on both the initial load and "Load more". - The legacy client-side filter is removed; the backend is the source of truth so results no longer depend on which cases happen to be loaded. - Empty state is now context-aware — when a search is active it reads "No results match your search \"<term>\"." instead of the misleading "No open cases. Run a measure to generate cases." - Replaced two dev-speak copy strings in the page header / subhead with user-facing descriptions. Docs (`docs/WALKTHROUGH_GUIDE.md` Section 4) - Rewrote the section to match the implementation: - clarified the dual `Cases` / `Worklist` sidebar entries - cards, not a table; Assignee / Next Action live on case detail - status filter is pill toggle buttons (Open / Closed / All / Excluded); Closed covers `CLOSED` and `RESOLVED` - All Cases / My Cases tabs documented - bulk-action labels corrected (Assign to… / Escalate selected / Export selected); separate `cases-selected.csv` download - documented the 25-per-page grid with the "Load more cases" button - two export buttons (Export cases CSV, Export audit CSV) with the full v1 columns list Tests - `CaseControllerTest` stubs updated for the new 9-arg `listCases` signature. - `CaseControllerTest` runs green (`./gradlew.bat test --tests com.workwell.web.CaseControllerTest`). - Frontend `pnpm lint` and `tsc --noEmit` clean. - Bug reproduced against prod with Playwright: searching `emp-001` returned the misleading "No open cases" empty state because emp-001 was outside the first 25-case page load. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6b0897c94e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const timer = window.setTimeout(() => { | ||
| setSearchTerm(urlSearch); | ||
| }, 0); | ||
| const params = new URLSearchParams(searchParams.toString()); | ||
| const trimmed = searchTerm.trim(); | ||
| if (trimmed) { | ||
| params.set("search", trimmed); |
There was a problem hiding this comment.
Preserve URL-driven search changes
When urlSearch changes independently (for example via browser back/forward, a shared link, or another control updating query params), this effect treats any mismatch as a signal to rewrite the URL from stale searchTerm state instead of syncing state from the URL. After 300ms it can revert the new ?search= value, causing history navigation to “bounce back” and loading results for the old term rather than the URL the user navigated to.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 34fcaed.
Split the sync into two effects guarded by a lastWrittenSearchRef:
- URL → state pulls into the input whenever
urlSearchdiffers from what we last wrote. - state → URL debounces typing, updates the ref before
router.replace, so the URL→state effect treats the resulting URL change as our own write and skips it.
Browser back/forward and deep links now update both the input and the query without being overwritten.
Codex flagged that the single-direction URL sync clobbers external URL changes. When `urlSearch` updates from browser back/forward, a shared link, or another control mutating query params, the previous effect saw a mismatch with stale local `searchTerm` state and, after the 300 ms debounce, rewrote the URL back to the old term — bouncing history navigation back and loading results for the wrong query. Split the sync into two effects with a `lastWrittenSearchRef` guard: - URL → state pulls external URL changes into the input whenever `urlSearch` differs from what we last wrote. - state → URL debounces user typing and updates the ref before calling `router.replace`, so the URL→state effect treats the resulting URL change as our own write and skips it. Result: - Typing still updates the URL after 300 ms. - Browser back/forward and deep links update both the input and the query without being overwritten. - No regression on the original Section 4 search fix — the backend query still receives the active term from the URL. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Taleef7
left a comment
There was a problem hiding this comment.
Found one additional pagination consistency issue beyond the existing URL/search sync comment. Once those are addressed, the Section 4 behavior should line up with the UAT acceptance criteria.
| else if (siteId) params.set("site", siteId); | ||
| if (from) params.set("from", from); | ||
| if (to) params.set("to", to); | ||
| if (urlSearch.trim()) params.set("search", urlSearch.trim()); |
There was a problem hiding this comment.
loadCases scopes the initial request with effectiveAssignee when view === "mine", but loadMoreCases still only forwards assigneeFilter. In the My Cases tab, clicking Load more cases will drop the current-user assignee scope and append cases from everyone else, even though the PR now forwards the search term. Please mirror the same effective-assignee logic here so pagination stays consistent with the active view.
There was a problem hiding this comment.
Addressed in 72cbc0d — loadMoreCases now derives the same effectiveAssignee = view === 'mine' ? user.email : assigneeFilter value as loadCases before appending the next page, so the My Cases scope is preserved across pagination.
…#42) Review noted that `loadCases` scopes the initial request with `effectiveAssignee = view === "mine" ? user.email : assigneeFilter`, but `loadMoreCases` only forwarded the raw `assigneeFilter`. On the "My Cases" tab, clicking **Load more cases** dropped the current-user assignee scope and appended cases from everyone else. Mirror the same effective-assignee logic in `loadMoreCases` so pagination stays consistent with the active view. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Addresses UAT findings in #27 (Section 4 of the Cases Worklist).
Code
GET /api/casesaccepts an optionalsearchparam.CaseFlowService.listCasesadds a case-insensitiveLIKEonemployees.name/employees.external_id, so search spans the whole worklist instead of being limited to the first 25 loaded cases on the client./cases?search=…and reloads from the backend.useMemoclient filter; backend is the source of truth.loadMoreCasesalso forwards the search term so pagination stays consistent.No results match your search "<term>".instead of the misleadingNo open cases. Run a measure to generate cases..Open worklist cases now persist from the seeded measure runs.→ friendlier description;Loaded from the DB-backed case endpoints.→Filter, search, and bulk-act on flagged cases.).Docs —
docs/WALKTHROUGH_GUIDE.mdSection 4Rewrote the section to match what the page actually does:
CasesvsWorklist).Assignee/Next Actionare not on the card.Open/Closed/All/Excluded);ClosedcoversCLOSEDandRESOLVED.All Cases/My Casestabs.Assign to…/Escalate selected/Export selected) and noted thecases-selected.csvdownload.Load more casesbutton.Export cases CSV,Export audit CSV) and the current v1 column list.Acceptance criteria from #27
Caseflow → Why Flagged casessubhead + section subhead).docs/WALKTHROUGH_GUIDE.md.Test plan
./gradlew.bat test --tests com.workwell.web.CaseControllerTest— passes after the 9-arg signature update.pnpm lintandnpx tsc --noEmitclean infrontend/.emp-001search → misleading empty state). Fix will need a deploy to verify against the live stack.🤖 Generated with Claude Code