Conversation
…sibility options - Implemented multi-status filtering for candidates using checkboxes. - Added score range filtering for candidate applications. - Introduced a column picker to toggle visibility of email, score, status, and applied date. - Enhanced sorting functionality for name, email, score, status, and applied date. - Updated UI to reflect active filters and provide clear filter options. - Improved empty state messaging for no candidates.
|
🚅 Deployed to the applirank-pr-40 environment in applirank
|
📝 WalkthroughWalkthroughBoth dashboard pages received enhanced filtering and sorting capabilities. Single-status filters were replaced with multi-select status filtering, score range inputs, and a collapsible filter panel. Sorting was added across multiple columns with visual indicators. Column visibility toggles and persistent state management enable users to customize table displays and filtering behavior. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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`:
- Line 90: The status filter only runs when more than one status is selected,
causing stale results for single selections; update the filtering condition in
the component's filter logic (where selectedStatuses is used and app.status is
checked) to run whenever any status is selected — e.g., change the guard from
selectedStatuses.value.length > 1 to selectedStatuses.value.length > 0 (so: if
(selectedStatuses.value.length > 0 &&
!selectedStatuses.value.includes(app.status as Status)) return false) ensuring
single-selection filters are applied immediately.
- Around line 62-66: activeFilterCount and the score-based filter checks treat
empty-string values from v-model.number as non-null, causing sticky filters; fix
by introducing computed proxies for scoreMin and scoreMax that normalize '' and
null to undefined (getter returns undefined for '', null; setter writes
normalized value to the reactive state), replace direct uses of
scoreMin/scoreMax in activeFilterCount and the filter predicate (where checks
currently use scoreMin.value != null / scoreMax.value != null) with the
normalized computed proxies, and update the input bindings for the number fields
to use these computed proxies so the filter logic and the filter pill display
(which reads score values) see undefined for cleared inputs rather than ''.
In `@app/pages/dashboard/jobs/`[id]/candidates.vue:
- Around line 151-152: The status filter currently only applies when
selectedStatuses.value.length > 1, which skips filtering for a single selection;
change the condition in the .filter callback to apply whenever
selectedStatuses.value is non-empty (e.g., selectedStatuses.value.length > 0) so
that if selectedStatuses.value does not include app.status (app.status as
Status) the row is filtered out locally to keep the UI consistent until API
refetch completes.
- Around line 112-116: The computed activeFilterCount is using raw
scoreMin/scoreMax which can be '' (from v-model.number) or NaN and still pass
the != null checks, so create proxy computed properties (e.g.,
normalizedScoreMin and normalizedScoreMax) that return undefined when the source
is '' or Number.isNaN(value) and otherwise return the numeric value; then update
activeFilterCount and any other logic that reads scoreMin/scoreMax (including
the other places noted around the component) to use
normalizedScoreMin/normalizedScoreMax instead of scoreMin/scoreMax so cleared
inputs no longer act as a 0 filter.
| const activeFilterCount = computed(() => { | ||
| let n = selectedStatuses.value.length | ||
| if (scoreMin.value != null) n++ | ||
| if (scoreMax.value != null) n++ | ||
| return n |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Vue 3 v-model.number empty input number field behavior
💡 Result:
In Vue 3, v-model.number does not guarantee your model is always a number:
- Vue casts using
parseFloat(). If parsing fails, it keeps the original string. (vuejs.org) - When the user clears the input, Vue sets the bound value to
''(empty string), notnull,0, orNaN. (vuejs.org) - The
.numberbehavior is applied automatically for<input type="number">even without writing.number. (vuejs.org) - This differs from the DOM API
valueAsNumber(e.g., empty input →NaN). (vuejs.org)
If you need “empty means null (or undefined)”, you must handle it yourself (e.g., in an @input handler or a computed setter) because Vue intentionally returns '' for empty. (vuejs.org)
Citations:
- 1: https://vuejs.org/guide/essentials/forms.html?utm_source=openai
- 2: https://vuejs.org/guide/essentials/forms.html?utm_source=openai
- 3: https://vuejs.org/guide/essentials/forms.html?utm_source=openai
- 4: https://vuejs.org/guide/essentials/forms.html?utm_source=openai
- 5: https://vuejs.org/guide/essentials/forms.html?utm_source=openai
🏁 Script executed:
# Find and inspect the dashboard applications index.vue file
fd -t f "index.vue" | grep -E "applications.*index\.vue"Repository: applirank/applirank
Length of output: 105
🏁 Script executed:
# Read the file to inspect the code at the mentioned lines
wc -l app/pages/dashboard/applications/index.vueRepository: applirank/applirank
Length of output: 109
🏁 Script executed:
# Get the relevant sections
cat -n app/pages/dashboard/applications/index.vue | sed -n '50,80p'Repository: applirank/applirank
Length of output: 1276
🏁 Script executed:
# Check the other mentioned lines
cat -n app/pages/dashboard/applications/index.vue | sed -n '85,100p'Repository: applirank/applirank
Length of output: 816
🏁 Script executed:
# Check lines around 263-279
cat -n app/pages/dashboard/applications/index.vue | sed -n '260,280p'Repository: applirank/applirank
Length of output: 1687
🏁 Script executed:
# Check lines around 309-314
cat -n app/pages/dashboard/applications/index.vue | sed -n '305,320p'Repository: applirank/applirank
Length of output: 806
🏁 Script executed:
# Find where scoreMin and scoreMax are defined
rg -n "const scoreMin|const scoreMax|useState.*score" app/pages/dashboard/applications/index.vueRepository: applirank/applirank
Length of output: 310
Normalize score inputs to prevent sticky filters when cleared.
When a user clears a number input with v-model.number, Vue sets the value to '' (empty string), not undefined. The activeFilterCount logic at lines 62–66 and filter checks at lines 91–92 use != null comparisons, which remain true for empty strings, keeping filters visually active despite being semantically empty. Apply the suggested computed proxy with normalization (converting ''/null to undefined) at the state binding point.
This affects:
- Filter counting (lines 62–66)
- Filter logic (lines 91–92)
- Input bindings (lines 263, 273)
- Filter pill display (line 309)
Suggested fix
const scoreMin = useState<number | undefined>('app-filter-score-min', () => undefined)
const scoreMax = useState<number | undefined>('app-filter-score-max', () => undefined)
+const normalizeScore = (val: unknown) => {
+ if (val === '' || val == null) return undefined
+ const n = Number(val)
+ return Number.isFinite(n) ? n : undefined
+}
+const scoreMinInput = computed({
+ get: () => scoreMin.value ?? '',
+ set: val => { scoreMin.value = normalizeScore(val) },
+})
+const scoreMaxInput = computed({
+ get: () => scoreMax.value ?? '',
+ set: val => { scoreMax.value = normalizeScore(val) },
+})- <input
- v-model.number="scoreMin"
+ <input
+ v-model="scoreMinInput"
type="number"
min="0"
max="100"
placeholder="Min"- <input
- v-model.number="scoreMax"
+ <input
+ v-model="scoreMaxInput"
type="number"
min="0"
max="100"
placeholder="Max"📝 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.
| const activeFilterCount = computed(() => { | |
| let n = selectedStatuses.value.length | |
| if (scoreMin.value != null) n++ | |
| if (scoreMax.value != null) n++ | |
| return n | |
| const scoreMin = useState<number | undefined>('app-filter-score-min', () => undefined) | |
| const scoreMax = useState<number | undefined>('app-filter-score-max', () => undefined) | |
| const normalizeScore = (val: unknown) => { | |
| if (val === '' || val == null) return undefined | |
| const n = Number(val) | |
| return Number.isFinite(n) ? n : undefined | |
| } | |
| const scoreMinInput = computed({ | |
| get: () => scoreMin.value ?? '', | |
| set: val => { scoreMin.value = normalizeScore(val) }, | |
| }) | |
| const scoreMaxInput = computed({ | |
| get: () => scoreMax.value ?? '', | |
| set: val => { scoreMax.value = normalizeScore(val) }, | |
| }) |
🤖 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 62 - 66,
activeFilterCount and the score-based filter checks treat empty-string values
from v-model.number as non-null, causing sticky filters; fix by introducing
computed proxies for scoreMin and scoreMax that normalize '' and null to
undefined (getter returns undefined for '', null; setter writes normalized value
to the reactive state), replace direct uses of scoreMin/scoreMax in
activeFilterCount and the filter predicate (where checks currently use
scoreMin.value != null / scoreMax.value != null) with the normalized computed
proxies, and update the input bindings for the number fields to use these
computed proxies so the filter logic and the filter pill display (which reads
score values) see undefined for cleared inputs rather than ''.
| const sorted = computed(() => { | ||
| return [...applications.value] | ||
| .filter((app) => { | ||
| if (selectedStatuses.value.length > 1 && !selectedStatuses.value.includes(app.status as Status)) return false |
There was a problem hiding this comment.
Apply status filter for a single selection too.
Filtering only when multiple statuses are selected can show stale results until the server refetch completes.
🔧 Suggested fix
- if (selectedStatuses.value.length > 1 && !selectedStatuses.value.includes(app.status as Status)) return false
+ if (selectedStatuses.value.length > 0 && !selectedStatuses.value.includes(app.status as Status)) return false📝 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.
| if (selectedStatuses.value.length > 1 && !selectedStatuses.value.includes(app.status as Status)) return false | |
| if (selectedStatuses.value.length > 0 && !selectedStatuses.value.includes(app.status as Status)) return false |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/pages/dashboard/applications/index.vue` at line 90, The status filter
only runs when more than one status is selected, causing stale results for
single selections; update the filtering condition in the component's filter
logic (where selectedStatuses is used and app.status is checked) to run whenever
any status is selected — e.g., change the guard from
selectedStatuses.value.length > 1 to selectedStatuses.value.length > 0 (so: if
(selectedStatuses.value.length > 0 &&
!selectedStatuses.value.includes(app.status as Status)) return false) ensuring
single-selection filters are applied immediately.
| const activeFilterCount = computed(() => { | ||
| let n = selectedStatuses.value.length | ||
| if (scoreMin.value != null) n++ | ||
| if (scoreMax.value != null) n++ | ||
| return n |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Vue 3.5 v-model.number cleared input empty string behavior
💡 Result:
In Vue 3.5, v-model.number will still produce an empty string ('') when the user clears the input.
Reason: the .number modifier casts using parseFloat(). If parsing fails, Vue falls back to the original string value—and for a cleared field that original value is '' (this is explicitly documented, and noted as different from the DOM valueAsNumber behavior). [1]
Common workaround (if you want null instead of ''):
<input
:value="modelValue ?? ''"
`@input`="modelValue = $event.target.value === '' ? null : Number($event.target.value)"
>Source: Vue Form Input Bindings docs (v-model modifiers → .number). [1]
References
[1] Vue.js docs – Form Input Bindings / .number modifier https://vuejs.org/guide/essentials/forms.html
🏁 Script executed:
find . -name "candidates.vue" -type fRepository: applirank/applirank
Length of output: 109
🏁 Script executed:
wc -l ./app/pages/dashboard/jobs/[id]/candidates.vueRepository: applirank/applirank
Length of output: 113
🏁 Script executed:
sed -n '1,50p' ./app/pages/dashboard/jobs/[id]/candidates.vue | head -40Repository: applirank/applirank
Length of output: 1457
🏁 Script executed:
sed -n '110,120p' ./app/pages/dashboard/jobs/[id]/candidates.vueRepository: applirank/applirank
Length of output: 366
🏁 Script executed:
sed -n '150,160p' ./app/pages/dashboard/jobs/[id]/candidates.vueRepository: applirank/applirank
Length of output: 519
🏁 Script executed:
sed -n '340,370p' ./app/pages/dashboard/jobs/[id]/candidates.vueRepository: applirank/applirank
Length of output: 1962
🏁 Script executed:
sed -n '386,400p' ./app/pages/dashboard/jobs/[id]/candidates.vueRepository: applirank/applirank
Length of output: 629
Normalize score inputs so clearing doesn't apply a 0 filter.
v-model.number yields '' when cleared, which fails the != null checks and gets coerced to 0 in comparisons. This causes cleared score filters to remain active and filter unexpectedly. Coerce '' and NaN to undefined via a proxy computed.
🛠️ Suggested fix
const scoreMin = useState<number | undefined>(`cand-filter-score-min-${jobId}`, () => undefined)
const scoreMax = useState<number | undefined>(`cand-filter-score-max-${jobId}`, () => undefined)
+const normalizeScore = (val: unknown) => {
+ if (val === '' || val == null) return undefined
+ const n = Number(val)
+ return Number.isFinite(n) ? n : undefined
+}
+const scoreMinInput = computed({
+ get: () => scoreMin.value ?? '',
+ set: val => { scoreMin.value = normalizeScore(val) },
+})
+const scoreMaxInput = computed({
+ get: () => scoreMax.value ?? '',
+ set: val => { scoreMax.value = normalizeScore(val) },
+})- <input
- v-model.number="scoreMin"
+ <input
+ v-model="scoreMinInput"
type="number"
min="0"
max="100"
placeholder="Min"- <input
- v-model.number="scoreMax"
+ <input
+ v-model="scoreMaxInput"
type="number"
min="0"
max="100"
placeholder="Max"Also applies to: 153-154, 392-397
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/pages/dashboard/jobs/`[id]/candidates.vue around lines 112 - 116, The
computed activeFilterCount is using raw scoreMin/scoreMax which can be '' (from
v-model.number) or NaN and still pass the != null checks, so create proxy
computed properties (e.g., normalizedScoreMin and normalizedScoreMax) that
return undefined when the source is '' or Number.isNaN(value) and otherwise
return the numeric value; then update activeFilterCount and any other logic that
reads scoreMin/scoreMax (including the other places noted around the component)
to use normalizedScoreMin/normalizedScoreMax instead of scoreMin/scoreMax so
cleared inputs no longer act as a 0 filter.
| .filter((app) => { | ||
| if (selectedStatuses.value.length > 1 && !selectedStatuses.value.includes(app.status as Status)) return false |
There was a problem hiding this comment.
Apply status filter even for a single selection.
Relying solely on the API can show stale rows until refetch completes; filtering any non‑empty selection keeps the UI consistent.
🔧 Suggested fix
- if (selectedStatuses.value.length > 1 && !selectedStatuses.value.includes(app.status as Status)) return false
+ if (selectedStatuses.value.length > 0 && !selectedStatuses.value.includes(app.status as Status)) return false📝 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.
| .filter((app) => { | |
| if (selectedStatuses.value.length > 1 && !selectedStatuses.value.includes(app.status as Status)) return false | |
| .filter((app) => { | |
| if (selectedStatuses.value.length > 0 && !selectedStatuses.value.includes(app.status as Status)) return false |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/pages/dashboard/jobs/`[id]/candidates.vue around lines 151 - 152, The
status filter currently only applies when selectedStatuses.value.length > 1,
which skips filtering for a single selection; change the condition in the
.filter callback to apply whenever selectedStatuses.value is non-empty (e.g.,
selectedStatuses.value.length > 0) so that if selectedStatuses.value does not
include app.status (app.status as Status) the row is filtered out locally to
keep the UI consistent until API refetch completes.
Summary
Type of change
Validation
DCO
Signed-off-by) viagit commit -sSummary by CodeRabbit