Add candidate self-service: profile edit, invitation response, work log appeal, payments#5
Conversation
`buildOSCommands()` never set the `shortcut` property on command objects, so the chord handler never matched G+H, G+C, G+R, G+T, G+O chords. Added a chord-to-href map keyed by role and applied shortcuts to both nav and scope commands. Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add tailwindcss, @tailwindcss/postcss, postcss, and autoprefixer as devDependencies. Create postcss.config.mjs with the Tailwind v4 PostCSS plugin. Add @import "tailwindcss" and @theme block to styles.css mapping existing CSS custom properties to Tailwind design tokens. Co-Authored-By: Paperclip <noreply@paperclip.ing>
…og appeal, payments Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
WalkthroughThis PR extends the candidate workspace with self-service profile management (including uploads, skills, experiences), invitation response and appeal forms, a payments/transfers view, workspace tabs UI, server actions and lookups, navigation updates, and Tailwind/CSS styling. ChangesCandidate workspace enhancement
Sequence DiagramssequenceDiagram
participant CandidateEditPage
participant getCandidateDetail
participant CandidateEditForm
participant updateCandidateProfile
CandidateEditPage->>getCandidateDetail: fetch candidate, countries, universities
getCandidateDetail-->>CandidateEditPage: candidate data with profile fields
CandidateEditPage->>CandidateEditForm: render with candidate props
CandidateEditForm->>updateCandidateProfile: submit profile form data
updateCandidateProfile-->>CandidateEditForm: success or error
sequenceDiagram
participant InvitationPage
participant InvitationRespondForm
participant respondToInvitation
InvitationRespondForm->>respondToInvitation: submit accept/reject with invitationUuid
respondToInvitation->>respondToInvitation: validate and update invitation status
respondToInvitation-->>InvitationRespondForm: success or error
respondToInvitation->>InvitationPage: redirect to updated invitation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
src/app/styles.cssParsing error: This experimental syntax requires enabling one of the following parser plugin(s): "decorators", "decorators-legacy". (1:0) Comment |
…ate edit Extends profile edit form with bank account details, inline skill/experience add and remove actions, and exposes status values on request pipeline rows. Co-Authored-By: Paperclip <noreply@paperclip.ing>
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (2)
src/modules/workspace/data.ts (1)
2-2: ⚡ Quick winSwitch internal import to
@/alias.Use the project alias for the
formatimport at Line 2 to match repository import rules.As per coding guidelines, "Use
@/path alias for all internal imports".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/modules/workspace/data.ts` at line 2, Update the import of the format utilities to use the project alias: replace the current relative import of formatDate and formatMoney (the import statement that references "./format") with the "`@/`..." alias form so the module imports formatDate and formatMoney via the project's path alias; ensure you only change the import path and keep the imported symbols (formatDate, formatMoney) intact.src/modules/candidates/CandidateEditForm.tsx (1)
11-11: ⚡ Quick winUse the
@/alias for internal imports.Replace the relative import at Line 11 with the project alias form for consistency and rule compliance.
As per coding guidelines, "Use
@/path alias for all internal imports".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/modules/candidates/CandidateEditForm.tsx` at line 11, The import in CandidateEditForm.tsx currently uses a relative path ("./actions"); update it to use the project alias form (e.g., replace "./actions" with "`@/modules/candidates/actions`") so internal imports follow the `@/` convention and satisfy the linting rule; modify the import statement that imports from "./actions" accordingly and ensure any named imports remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/candidate/edit/page.tsx`:
- Around line 25-49: CandidateEditForm is being called without required fields
and props: the candidate object must include bankId, bankAccountName, and iban
(add them with appropriate values or safe defaults like null/"" using the same
pattern as other fields) and the component must be passed the required props
banks, skills, and experiences; update the call site to include candidate.bankId
/ candidate.bankAccountName / candidate.iban and add banks={banks}
skills={skills} experiences={experiences} (or the correct variables/empty arrays
if not yet available) so the prop contract for CandidateEditForm is satisfied.
In `@src/app/styles.css`:
- Around line 3-24: Stylelint is failing on the Tailwind v4 `@theme` at-rule used
in the CSS block; update the scss/at-rule-no-unknown rule in your stylelint
config to allow Tailwind at-rules by adding "theme", "source", and "utility" to
the ignoreAtRules list (i.e., modify the scss/at-rule-no-unknown rule in
.stylelintrc.json to include those at-rules so `@theme` and similar Tailwind
directives are accepted).
In `@src/modules/candidates/actions.ts`:
- Around line 105-115: The server currently only checks presence and logical
type before calling saveUpload, so harden validation by validating file MIME
type, extension, and size on the server side and sanitizing the filename before
saving; specifically, in the upload handler (the block that checks file
instanceof File, file.size, allowed array and then calls saveUpload) enforce
allowed MIME types and file extensions per document type (e.g., image/jpeg/png
for "photo", pdf/docx for "cv", mp4/webm for "video"), reject files over a
defined max size (e.g., 5MB for photos, larger for CV/video), verify the
uploaded file's actual content-type (not just client-provided) and/or inspect
magic bytes, sanitize or generate a safe filename, and ensure saveUpload writes
only into a safe directory under public/uploads with no path traversal; update
saveUpload to trust validated inputs or perform the same checks there if called
elsewhere.
In `@src/modules/candidates/CandidateEditForm.tsx`:
- Around line 147-167: The form currently renders multiple DocumentUpload
components that all submit the same field names ("type" and "file"), while the
server reads only one pair via formData.get(...), causing ambiguity; update the
client and/or server so each document uses unique field names: modify
DocumentUpload to set the file input name to a unique identifier (e.g.,
`file_${type}`) and, if you keep a single form with action={uploadAction},
include a hidden input name like `docType_${type}` or only submit one document
per request by moving DocumentUpload into its own <form> that posts a single
`type` and `file`; then update the upload handler to read files by those unique
names (e.g., loop supportedTypes and call formData.get(`file_${type}`) /
formData.get(`docType_${type}`)) or accept one-file-per-form when using separate
forms. Ensure you change references to the DocumentUpload prop `type` and the
server-side formData access to match the new naming scheme.
In `@src/modules/candidates/InvitationRespondForm.tsx`:
- Line 4: Replace the relative import of respondToInvitation in
InvitationRespondForm.tsx with the project alias import; locate the import
statement importing respondToInvitation from "./actions" and change it to use
the "`@/`..." alias path that points to the same module (e.g., import from
"`@/modules/candidates/actions`") so it conforms to the repo's internal import
conventions.
In `@src/modules/candidates/WorkLogAppealForm.tsx`:
- Line 4: Replace the relative import of the appealWorkLog symbol in
WorkLogAppealForm.tsx with the project alias form (using "`@/`...") to comply with
the internal import rule; locate the import statement that currently references
"./actions" and update it to import appealWorkLog from the equivalent "`@/`..."
module path so the same symbol is imported via the repository alias.
---
Nitpick comments:
In `@src/modules/candidates/CandidateEditForm.tsx`:
- Line 11: The import in CandidateEditForm.tsx currently uses a relative path
("./actions"); update it to use the project alias form (e.g., replace
"./actions" with "`@/modules/candidates/actions`") so internal imports follow the
`@/` convention and satisfy the linting rule; modify the import statement that
imports from "./actions" accordingly and ensure any named imports remain
unchanged.
In `@src/modules/workspace/data.ts`:
- Line 2: Update the import of the format utilities to use the project alias:
replace the current relative import of formatDate and formatMoney (the import
statement that references "./format") with the "`@/`..." alias form so the module
imports formatDate and formatMoney via the project's path alias; ensure you only
change the import path and keep the imported symbols (formatDate, formatMoney)
intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: fc8bdf80-1515-4b9d-9229-0057225f4b06
📒 Files selected for processing (13)
src/app/candidate/edit/page.tsxsrc/app/candidate/invitations/[id]/page.tsxsrc/app/candidate/page.tsxsrc/app/candidate/payments/page.tsxsrc/app/candidate/work-logs/[id]/page.tsxsrc/app/styles.csssrc/modules/candidates/CandidateEditForm.tsxsrc/modules/candidates/InvitationRespondForm.tsxsrc/modules/candidates/WorkLogAppealForm.tsxsrc/modules/candidates/actions.tssrc/modules/workspace/data.tssrc/modules/workspace/navigation.tstsconfig.json
| @theme { | ||
| --color-background: var(--background); | ||
| --color-foreground: var(--foreground); | ||
| --color-card: var(--card); | ||
| --color-card-foreground: var(--card-foreground); | ||
| --color-popover: var(--popover); | ||
| --color-popover-foreground: var(--popover-foreground); | ||
| --color-primary: var(--primary); | ||
| --color-primary-foreground: var(--primary-foreground); | ||
| --color-secondary: var(--secondary); | ||
| --color-secondary-foreground: var(--secondary-foreground); | ||
| --color-muted: var(--muted-shadcn); | ||
| --color-muted-foreground: var(--muted-foreground); | ||
| --color-accent: var(--accent); | ||
| --color-accent-foreground: var(--accent-foreground); | ||
| --color-destructive: var(--destructive); | ||
| --color-destructive-foreground: var(--destructive-foreground); | ||
| --color-border: var(--border); | ||
| --color-input: var(--input); | ||
| --color-ring: var(--ring); | ||
| --radius: var(--radius); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find stylelint config files
fd -HI 'stylelint.config.*|.stylelintrc.*|.stylelintrc' .
# Verify current at-rule settings and whether `@theme` is allowed
rg -n --hidden -S 'scss/at-rule-no-unknown|at-rule-no-unknown|ignoreAtRules|`@theme`|`@source`|`@utility`'Repository: BAWES/studenthub-codex
Length of output: 116
🏁 Script executed:
cat .stylelintrc.jsonRepository: BAWES/studenthub-codex
Length of output: 1426
🏁 Script executed:
# Check Tailwind version and stylelint setup in package.json
grep -A 5 -B 5 'tailwind\|stylelint' package.json | head -30Repository: BAWES/studenthub-codex
Length of output: 619
🏁 Script executed:
# Check if there are any other stylelint rules related to at-rules
rg -A 10 'rules.*:' .stylelintrc.jsonRepository: BAWES/studenthub-codex
Length of output: 480
🏁 Script executed:
# Check if scss/at-rule-no-unknown is explicitly in the rules
rg -i 'scss/at-rule-no-unknown|ignoreAtRules' .stylelintrc.jsonRepository: BAWES/studenthub-codex
Length of output: 48
🏁 Script executed:
# Get the full rules section from .stylelintrc.json to see all configured rules
cat .stylelintrc.json | jq '.rules | keys' 2>/dev/null || cat .stylelintrc.jsonRepository: BAWES/studenthub-codex
Length of output: 1105
🏁 Script executed:
# Check if there's a postcss.config or tailwind.config that might handle this
fd -HI 'postcss.config.*|tailwind.config.*' . | head -10Repository: BAWES/studenthub-codex
Length of output: 87
🏁 Script executed:
cat ./postcss.config.mjsRepository: BAWES/studenthub-codex
Length of output: 182
🏁 Script executed:
# Also check if there's any reference to ignoreAtRules in docs or if it needs to be added
# Search entire codebase for any stylelint-related configuration
rg -i 'ignoreAtRules|ignoreUnknownAtRules' .Repository: BAWES/studenthub-codex
Length of output: 48
Add scss/at-rule-no-unknown rule to allow Tailwind v4 at-rules.
Lines 3-24 use Tailwind v4's @theme at-rule, which is valid CSS but causes stylelint to fail on scss/at-rule-no-unknown (active in the inherited stylelint-config-standard-scss base config). Update .stylelintrc.json to ignore or allow Tailwind at-rules:
{
"rules": {
"scss/at-rule-no-unknown": [
true,
{
"ignoreAtRules": ["theme", "source", "utility"]
}
]
}
}🧰 Tools
🪛 Stylelint (17.11.1)
[error] 3-3: Unexpected unknown at-rule "@theme" (scss/at-rule-no-unknown)
(scss/at-rule-no-unknown)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/styles.css` around lines 3 - 24, Stylelint is failing on the Tailwind
v4 `@theme` at-rule used in the CSS block; update the scss/at-rule-no-unknown rule
in your stylelint config to allow Tailwind at-rules by adding "theme", "source",
and "utility" to the ignoreAtRules list (i.e., modify the
scss/at-rule-no-unknown rule in .stylelintrc.json to include those at-rules so
`@theme` and similar Tailwind directives are accepted).
| <form action={uploadAction} className="candidateEditForm"> | ||
| <h2>Documents</h2> | ||
| {uploadState.error ? <p className="formError">{uploadState.error}</p> : null} | ||
|
|
||
| <DocumentUpload label="Profile photo" type="photo" current={candidate.personalPhoto} /> | ||
|
|
||
| <DocumentUpload label="CV / Resume" type="cv" current={candidate.resume} /> | ||
|
|
||
| <DocumentUpload label="Video" type="video" current={candidate.video} /> | ||
|
|
||
| <DocumentUpload | ||
| label="Civil ID (front)" | ||
| type="civilFront" | ||
| current={candidate.civilPhotoFront} | ||
| /> | ||
|
|
||
| <DocumentUpload | ||
| label="Civil ID (back)" | ||
| type="civilBack" | ||
| current={candidate.civilPhotoBack} | ||
| /> |
There was a problem hiding this comment.
Document upload form cannot reliably map selected file to the intended document type.
At Line 147 and Line 191-193, all document sections submit repeated type and file fields in one form, but the server reads only one type/file pair via formData.get(...). This makes uploads ambiguous (wrong target field or rejected upload).
Also applies to: 191-193
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/modules/candidates/CandidateEditForm.tsx` around lines 147 - 167, The
form currently renders multiple DocumentUpload components that all submit the
same field names ("type" and "file"), while the server reads only one pair via
formData.get(...), causing ambiguity; update the client and/or server so each
document uses unique field names: modify DocumentUpload to set the file input
name to a unique identifier (e.g., `file_${type}`) and, if you keep a single
form with action={uploadAction}, include a hidden input name like
`docType_${type}` or only submit one document per request by moving
DocumentUpload into its own <form> that posts a single `type` and `file`; then
update the upload handler to read files by those unique names (e.g., loop
supportedTypes and call formData.get(`file_${type}`) /
formData.get(`docType_${type}`)) or accept one-file-per-form when using separate
forms. Ensure you change references to the DocumentUpload prop `type` and the
server-side formData access to match the new naming scheme.
Expands readiness from 8 broad categories to 14 specific field-level checks (name, email, phone, country, university, objective, civil ID, profile photo, CV, bank info, skills, experience, approval) and surfaces exact missing field names so candidates know precisely what to fill. Co-Authored-By: Paperclip <noreply@paperclip.ing>
…on types Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Use @/ path alias for all internal imports (CandidateEditForm, InvitationRespondForm, WorkLogAppealForm, data.ts) - Add per-type MIME, extension, and size validation in uploadDocument - Add try/catch around saveUpload for clean error UX Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/modules/candidates/CandidateEditForm.tsx (1)
175-195:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDocument upload submission is still ambiguous and can target the wrong document type.
Line 301 and Line 302 are repeated for every document block inside one form, but the server action reads a single
type/filepair. In practice this can submit the wrong file/type combination or fail when the first file input is empty.Proposed fix
-<form action={uploadAction} className="candidateEditForm"> +<div className="candidateEditForm"> <h2>Documents</h2> {uploadState.error ? <p className="formError">{uploadState.error}</p> : null} - - <DocumentUpload label="Profile photo" type="photo" current={candidate.personalPhoto} /> - <DocumentUpload label="CV / Resume" type="cv" current={candidate.resume} /> - ... - - <div className="formActions"> - <button type="submit" disabled={uploadPending}> - {uploadPending ? "Uploading..." : "Upload document"} - </button> - </div> -</form> + <DocumentUpload label="Profile photo" type="photo" current={candidate.personalPhoto} action={uploadAction} pending={uploadPending} /> + <DocumentUpload label="CV / Resume" type="cv" current={candidate.resume} action={uploadAction} pending={uploadPending} /> + ... +</div>-function DocumentUpload({ label, type, current }: { ... }) { +function DocumentUpload({ label, type, current, action, pending }: { ...; action: (formData: FormData) => void; pending: boolean }) { return ( - <fieldset className="documentUploadField"> + <form action={action} className="documentUploadField"> + <fieldset> <legend>{label}</legend> <input type="hidden" name="type" value={type} /> <input type="file" name="file" accept={acceptFor(type)} /> + <button type="submit" disabled={pending}>{pending ? "Uploading..." : `Upload ${label}`}</button> ... - </fieldset> + </fieldset> + </form> ); }Also applies to: 301-303
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/modules/candidates/CandidateEditForm.tsx` around lines 175 - 195, The form currently wraps multiple DocumentUpload components but the server action (uploadAction) expects a single type/file pair, causing ambiguous/mismatched submissions; fix by making each DocumentUpload submit independently: either change DocumentUpload to render its own <form action={uploadAction}> with a hidden input for the document type and its file input + submit button, or split the existing form into separate forms around each DocumentUpload so each submission includes exactly one type/file pair; update any uploadState handling to reflect per-document responses and keep uploadAction unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/modules/candidates/CandidateEditForm.tsx`:
- Around line 204-217: The skills list nests a remove <form> inside the parent
addSkill form (using addSkillAction), which is invalid and causes the remove
action to submit the wrong form; fix by removing the inner <form> around the
"Remove" button and instead wire the remove action so it does not live inside
the parent form — for example create a standalone form element (per skill)
outside the parent add form or use a button with a form="{uniqueId}" attribute
that targets a separate form with action={removeSkillAction} and includes the
hidden skillId; use the same approach for the experience remove controls (the
removeSkillAction / removeSkillPending symbols and the corresponding experience
remove action/button) so no forms are nested.
In `@src/modules/workspace/WorkTabs.tsx`:
- Around line 89-104: closeTab currently calls setTabs and then reads the outer
tabs variable causing a stale-closure; move the navigation logic into the
setTabs updater so it uses the freshly computed next state: inside the updater
filter out the tab (as you already do), call writeTabs(next), and if pathname
=== path check remaining from next and call router.push(remaining[0].path as
Route) as needed; then return next and remove tabs from the useCallback
dependency list (keep pathname and router).
---
Duplicate comments:
In `@src/modules/candidates/CandidateEditForm.tsx`:
- Around line 175-195: The form currently wraps multiple DocumentUpload
components but the server action (uploadAction) expects a single type/file pair,
causing ambiguous/mismatched submissions; fix by making each DocumentUpload
submit independently: either change DocumentUpload to render its own <form
action={uploadAction}> with a hidden input for the document type and its file
input + submit button, or split the existing form into separate forms around
each DocumentUpload so each submission includes exactly one type/file pair;
update any uploadState handling to reflect per-document responses and keep
uploadAction unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 1ced9136-97e4-4487-b170-c29cc3fc66ab
📒 Files selected for processing (10)
src/app/candidate/edit/page.tsxsrc/app/styles.csssrc/modules/candidates/CandidateEditForm.tsxsrc/modules/candidates/CandidateProfile.tsxsrc/modules/candidates/InvitationRespondForm.tsxsrc/modules/candidates/WorkLogAppealForm.tsxsrc/modules/candidates/actions.tssrc/modules/requests/application-actions.tssrc/modules/workspace/WorkTabs.tsxsrc/modules/workspace/data.ts
| <form action={addSkillAction} className="candidateEditForm"> | ||
| <h2>Skills</h2> | ||
|
|
||
| {skills.length ? ( | ||
| <ul className="editableList"> | ||
| {skills.map((s) => ( | ||
| <li key={s.id}> | ||
| <span>{s.title}</span> | ||
| <form action={removeSkillAction}> | ||
| <input type="hidden" name="skillId" value={s.id} /> | ||
| <button type="submit" disabled={removeSkillPending} className="removeButton"> | ||
| Remove | ||
| </button> | ||
| </form> |
There was a problem hiding this comment.
Nested <form> elements create invalid markup and break remove actions.
Line 212 and Line 245 place forms inside parent forms. Browsers do not support nested forms reliably, so remove actions may submit the outer add form instead of the intended remove action.
Proposed fix
-<form action={addSkillAction} className="candidateEditForm">
+<form action={addSkillAction} className="candidateEditForm">
...
{skills.map((s) => (
<li key={s.id}>
<span>{s.title}</span>
- <form action={removeSkillAction}>
- <input type="hidden" name="skillId" value={s.id} />
- <button type="submit" disabled={removeSkillPending} className="removeButton">
- Remove
- </button>
- </form>
+ <button
+ type="submit"
+ formAction={removeSkillAction}
+ name="skillId"
+ value={String(s.id)}
+ disabled={removeSkillPending}
+ className="removeButton"
+ >
+ Remove
+ </button>
</li>
))}Apply the same pattern to the experience remove button (Line 245–250).
Also applies to: 237-250
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/modules/candidates/CandidateEditForm.tsx` around lines 204 - 217, The
skills list nests a remove <form> inside the parent addSkill form (using
addSkillAction), which is invalid and causes the remove action to submit the
wrong form; fix by removing the inner <form> around the "Remove" button and
instead wire the remove action so it does not live inside the parent form — for
example create a standalone form element (per skill) outside the parent add form
or use a button with a form="{uniqueId}" attribute that targets a separate form
with action={removeSkillAction} and includes the hidden skillId; use the same
approach for the experience remove controls (the removeSkillAction /
removeSkillPending symbols and the corresponding experience remove
action/button) so no forms are nested.
| const closeTab = useCallback( | ||
| (path: string) => { | ||
| setTabs((prev) => { | ||
| const next = prev.filter((t) => t.path !== path); | ||
| writeTabs(next); | ||
| return next; | ||
| }); | ||
| if (pathname === path) { | ||
| const remaining = tabs.filter((t) => t.path !== path); | ||
| if (remaining.length > 0) { | ||
| router.push(remaining[0].path as Route); | ||
| } | ||
| } | ||
| }, | ||
| [pathname, router, tabs] | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify the stale-state pattern exists in closeTab
rg -n -C3 'const closeTab|tabs\.filter\(\(t\) => t\.path !== path\)|setTabs\(\(prev\)' src/modules/workspace/WorkTabs.tsxRepository: BAWES/studenthub-codex
Length of output: 761
🏁 Script executed:
# Get the full context of the closeTab function and surrounding code
sed -n '70,110p' src/modules/workspace/WorkTabs.tsxRepository: BAWES/studenthub-codex
Length of output: 1081
🏁 Script executed:
# Search for all usages of closeTab in the file to understand its usage pattern
rg -n 'closeTab' src/modules/workspace/WorkTabs.tsxRepository: BAWES/studenthub-codex
Length of output: 190
🏁 Script executed:
# Check if there are other state dependencies or effects that interact with this function
rg -n 'useEffect|useCallback|useRef|useState' src/modules/workspace/WorkTabs.tsx | head -20Repository: BAWES/studenthub-codex
Length of output: 304
Move tab navigation into the state updater to eliminate stale-state closure.
closeTab schedules setTabs then immediately reads the outer tabs snapshot to determine navigation, creating a stale-closure pattern. Move the navigation logic inside the setTabs updater to use the fresh next state, and remove tabs from dependencies—this is more efficient and aligns with how addTab structures its state updates.
Proposed fix
const closeTab = useCallback(
(path: string) => {
setTabs((prev) => {
const next = prev.filter((t) => t.path !== path);
writeTabs(next);
+ if (pathname === path && next.length > 0) {
+ router.push(next[0].path as Route);
+ }
return next;
});
- if (pathname === path) {
- const remaining = tabs.filter((t) => t.path !== path);
- if (remaining.length > 0) {
- router.push(remaining[0].path as Route);
- }
- }
},
- [pathname, router, tabs]
+ [pathname, router]
);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/modules/workspace/WorkTabs.tsx` around lines 89 - 104, closeTab currently
calls setTabs and then reads the outer tabs variable causing a stale-closure;
move the navigation logic into the setTabs updater so it uses the freshly
computed next state: inside the updater filter out the tab (as you already do),
call writeTabs(next), and if pathname === path check remaining from next and
call router.push(remaining[0].path as Route) as needed; then return next and
remove tabs from the useCallback dependency list (keep pathname and router).
Summary
/candidate/editwith full field coverage (name, email, phone, birth date, country, university, address, civil ID, objective, intro, profile URL) plus document uploads (photo, CV, video, civil ID)/candidate/invitations/[id]usinguseActionState/candidate/work-logs/[id]with reason textarea, creates appeal record + links it to the work log in a transaction/candidate/paymentspage reading fromtransfer_candidatewith format helperse2e/andplaywright.config.tsfrom tsconfig so Next.js build passes without Playwright types installedTest plan
npm run buildpasses/candidate/edit, save a field, verify it persists/candidate/invitations/[uuid]/candidate/work-logs/[uuid]/candidate/paymentsand verify transfer rows render🤖 Generated with Claude Code
Summary by CodeRabbit