Skip to content

Ux/improve-application-view#46

Merged
JoachimLK merged 5 commits intomainfrom
ux/improve-application-view
Feb 26, 2026
Merged

Ux/improve-application-view#46
JoachimLK merged 5 commits intomainfrom
ux/improve-application-view

Conversation

@JoachimLK
Copy link
Copy Markdown
Contributor

@JoachimLK JoachimLK commented Feb 26, 2026

Summary

  • What does this PR change?
  • Why is this needed?

Type of change

  • Bug fix
  • [ x] Feature
  • Refactor
  • Docs
  • Chore

Validation

  • I tested locally
  • I added/updated relevant documentation
  • I verified multi-tenant scoping and auth behavior for affected API paths

DCO

  • All commits in this PR are signed off (Signed-off-by) via git commit -s

Summary by CodeRabbit

  • New Features
    • Added search functionality across candidate names, emails, and job titles
    • Redesigned filtering interface with job, status, and score filters
    • Added configurable sorting options (creation date, score, candidate name)
    • Changed applications view from table to card-based layout with candidate details, job information, and location

@railway-app railway-app Bot temporarily deployed to applirank / applirank-pr-46 February 26, 2026 12:19 Destroyed
@railway-app
Copy link
Copy Markdown

railway-app Bot commented Feb 26, 2026

🚅 Deployed to the applirank-pr-46 environment in applirank

Service Status Web Updated (UTC)
applirank ✅ Success (View Logs) Feb 26, 2026 at 12:43 pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 26, 2026

Warning

Rate limit exceeded

@JoachimLK has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 34 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between f4f29d5 and 170ec76.

📒 Files selected for processing (2)
  • app/pages/dashboard/jobs/[id]/swipe.vue
  • railway.json
📝 Walkthrough

Walkthrough

A comprehensive search and filter system has been implemented across the applications page, introducing debounced search input, job/status/score/sort dropdowns, and refactoring the UI from a table to a card-based grid layout. Backend API support includes ilike-based search filtering on candidate names, emails, and job titles, with schema validation for the new search parameter.

Changes

Cohort / File(s) Summary
Frontend Application Page
app/pages/dashboard/applications/index.vue
Major UI refactor replacing status-based multi-select with debounced search input, job/status/score/sort filter dropdowns, and card-based grid layout. Added filtering/sorting logic, active filter pills with individual clear actions, and improved empty/loading/error states.
Composable Integration
app/composables/useApplications.ts
Added optional search and jobId parameters to the composable options to propagate search and job filtering to the API query.
Backend API Filtering
server/api/applications/index.get.ts
Implemented search filtering using ilike comparisons on candidate firstName, lastName, email, and job title. Enhanced total count calculation with dedicated query and joins. Added job.location field projection.
Schema Validation
server/utils/schemas/application.ts
Added optional search field to applicationQuerySchema with max length of 200 characters.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant Vue as Vue Component
    participant Composable as useApplications<br/>(Composable)
    participant API as API Endpoint<br/>(applications/index.get)
    participant DB as Database

    User->>Vue: Types in search input
    Vue->>Vue: Debounce search input (500ms)
    Vue->>Composable: Update debouncedSearch ref
    Composable->>Composable: Build query with search +<br/>jobId + status params
    Composable->>API: Fetch applications with<br/>search filter
    API->>API: Build ilike search conditions<br/>on firstName, lastName,<br/>email, job.title
    API->>DB: Execute query with search<br/>filter + joins for count
    DB->>API: Return filtered applications<br/>+ total count
    API->>Composable: Return paginated<br/>results + metadata
    Composable->>Vue: Update applications list
    Vue->>User: Render card grid with<br/>filtered results + active<br/>filter pills
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A search through the warren, so swift and divine,
With filters and dropdowns that perfectly align,
From debounced inputs to database queries deep,
The applications now dance in a card-laden heap,
Our warren's now searchable—hop, hop, hooray! 🥕✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description contains only placeholder template text with no actual implementation details, reasoning, or validation confirmations. Fill in the Summary section with what changed and why, confirm which validation steps were completed, and ensure all DCO commits are properly signed off.
Title check ❓ Inconclusive The title 'Ux/improve-application-view' is partially related but vague and uses branch naming convention rather than a clear summary. Use a descriptive title like 'Add search and job filtering to application view' that clearly summarizes the main changes without using branch naming patterns.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ux/improve-application-view

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@railway-app railway-app Bot temporarily deployed to applirank / applirank-pr-46 February 26, 2026 12:21 Destroyed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/pages/dashboard/applications/index.vue (1)

120-149: Client-side sorting operates only on the current page's data.

The sorting (and multi-status filtering) happens client-side on the already-paginated server response. When a user selects "Highest score" or "Name A–Z", they're only reordering the current page (default 20 items), not seeing the globally highest-scored or alphabetically first candidates across all applications.

This could lead to a confusing UX where the sorted results don't match user expectations for large datasets.

Consider either:

  1. Moving sort parameters to the server-side API (preferred for consistency)
  2. Adding a UI indicator that sorting applies only to the currently loaded results
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/applications/index.vue` around lines 120 - 149, The
computed "sorted" currently filters and sorts the already-paginated
applications.value on the client (using sortKey, sortDir, selectedStatuses,
scoreMin, scoreMax), which only reorders the current page; move sorting and
multi-status filtering to the server by sending sortKey, sortDir and the filter
params with the applications fetch request (update the fetch function that loads
applications to accept these parameters and apply them in the backend API call),
then have the component rely on the server-returned, globally-sorted/paginated
list (keep a lightweight client-side fallback for UI-only tweaks if needed), or
alternatively add a clear UI indicator near the sort controls stating "sorting
applies to current page only" if you choose to keep client-side behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/pages/dashboard/applications/index.vue`:
- Around line 21-27: The debounce timer started in the watch callback
(debounceTimer, watch(searchInput, ...), updating debouncedSearch) isn’t cleared
on component unmount; add an onUnmounted (or onBeforeUnmount) hook that calls
clearTimeout(debounceTimer) and resets debounceTimer to undefined, and ensure
debounceTimer’s type allows undefined so the cleanup is safe and prevents the
pending timer from trying to update debouncedSearch after the component is
destroyed.

In `@server/api/applications/index.get.ts`:
- Around line 28-38: The search term is used directly in a LIKE pattern so
user-supplied % and _ act as wildcards; add an escaping step before building
term: create a small helper (e.g., escapeLike) that first escapes backslashes,
then replaces '%' with '\%' and '_' with '\_', call escapeLike(query.search) and
use `%${escaped}%` when constructing the ilike conditions (the ilike calls
referencing candidate.firstName, candidate.lastName, candidate.email,
job.title). Ensure the escaped string is passed into ilike so literal '%' and
'_' are treated literally.

---

Nitpick comments:
In `@app/pages/dashboard/applications/index.vue`:
- Around line 120-149: The computed "sorted" currently filters and sorts the
already-paginated applications.value on the client (using sortKey, sortDir,
selectedStatuses, scoreMin, scoreMax), which only reorders the current page;
move sorting and multi-status filtering to the server by sending sortKey,
sortDir and the filter params with the applications fetch request (update the
fetch function that loads applications to accept these parameters and apply them
in the backend API call), then have the component rely on the server-returned,
globally-sorted/paginated list (keep a lightweight client-side fallback for
UI-only tweaks if needed), or alternatively add a clear UI indicator near the
sort controls stating "sorting applies to current page only" if you choose to
keep client-side behavior.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ab24d4 and f4f29d5.

📒 Files selected for processing (4)
  • app/composables/useApplications.ts
  • app/pages/dashboard/applications/index.vue
  • server/api/applications/index.get.ts
  • server/utils/schemas/application.ts

Comment on lines +21 to +27
let debounceTimer: ReturnType<typeof setTimeout>
watch(searchInput, (val) => {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
debouncedSearch.value = val.trim() || undefined
}, 300)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Debounce timer is not cleaned up on component unmount.

If the component is destroyed while the debounce timer is pending, it may attempt to update debouncedSearch after unmount, potentially causing warnings or unexpected behavior.

🛡️ Proposed fix to clean up timer on unmount
 let debounceTimer: ReturnType<typeof setTimeout>
 watch(searchInput, (val) => {
   clearTimeout(debounceTimer)
   debounceTimer = setTimeout(() => {
     debouncedSearch.value = val.trim() || undefined
   }, 300)
 })
+
+onUnmounted(() => {
+  clearTimeout(debounceTimer)
+})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let debounceTimer: ReturnType<typeof setTimeout>
watch(searchInput, (val) => {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
debouncedSearch.value = val.trim() || undefined
}, 300)
})
let debounceTimer: ReturnType<typeof setTimeout>
watch(searchInput, (val) => {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
debouncedSearch.value = val.trim() || undefined
}, 300)
})
onUnmounted(() => {
clearTimeout(debounceTimer)
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/applications/index.vue` around lines 21 - 27, The
debounce timer started in the watch callback (debounceTimer, watch(searchInput,
...), updating debouncedSearch) isn’t cleared on component unmount; add an
onUnmounted (or onBeforeUnmount) hook that calls clearTimeout(debounceTimer) and
resets debounceTimer to undefined, and ensure debounceTimer’s type allows
undefined so the cleanup is safe and prevents the pending timer from trying to
update debouncedSearch after the component is destroyed.

Comment on lines +28 to +38
if (query.search) {
const term = `%${query.search}%`
conditions.push(
or(
ilike(candidate.firstName, term),
ilike(candidate.lastName, term),
ilike(candidate.email, term),
ilike(job.title, term),
)!,
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

LIKE pattern characters are not escaped in search term.

User input containing % or _ characters will be interpreted as LIKE wildcards, allowing users to craft unintended patterns (e.g., searching % matches everything, _ matches single chars). While not a SQL injection risk due to parameterized queries, this can lead to unexpected search behavior.

🛡️ Proposed fix to escape LIKE special characters
 if (query.search) {
-  const term = `%${query.search}%`
+  const escapedSearch = query.search.replace(/[%_]/g, '\\$&')
+  const term = `%${escapedSearch}%`
   conditions.push(
     or(
       ilike(candidate.firstName, term),
       ilike(candidate.lastName, term),
       ilike(candidate.email, term),
       ilike(job.title, term),
     )!,
   )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/applications/index.get.ts` around lines 28 - 38, The search term
is used directly in a LIKE pattern so user-supplied % and _ act as wildcards;
add an escaping step before building term: create a small helper (e.g.,
escapeLike) that first escapes backslashes, then replaces '%' with '\%' and '_'
with '\_', call escapeLike(query.search) and use `%${escaped}%` when
constructing the ilike conditions (the ilike calls referencing
candidate.firstName, candidate.lastName, candidate.email, job.title). Ensure the
escaped string is passed into ilike so literal '%' and '_' are treated
literally.

@railway-app railway-app Bot temporarily deployed to applirank / applirank-pr-46 February 26, 2026 12:40 Destroyed
@JoachimLK JoachimLK merged commit d41e276 into main Feb 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant