Skip to content

Attach username to upload dropdown#1350

Open
david-roper wants to merge 26 commits intoDouglasNeuroInformatics:mainfrom
david-roper:username-upload-always
Open

Attach username to upload dropdown#1350
david-roper wants to merge 26 commits intoDouglasNeuroInformatics:mainfrom
david-roper:username-upload-always

Conversation

@david-roper
Copy link
Copy Markdown
Collaborator

@david-roper david-roper commented May 5, 2026

The other option to close issue #1342

This pr removes the optional checkbox and make it always attach the user to the uploaded data whenever possible.

If we select this option the pr #1344 will be removed

The new implementation now includes a dropdown of all the usernames in the group, which allows the user to select which username to attach to the upload. The dropdown defaults to the current user's username and also provides an 'N/A' option if needed.

small change to update user backend that fixes issue #1354

Summary by CodeRabbit

  • New Features

    • Option to select which username to associate with imported instrument records during upload.
  • Improvements

    • User list is scoped to the active group so the dropdown shows relevant members.
    • Upload payload now includes the chosen username and the client forwards the selection.
    • Upload UI refined with a cleaner centered layout and loading presentation.
  • Bug Fixes

    • Upload enforces that a chosen username belongs to the selected group.
  • Tests

    • Added tests covering username handling during upload.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds optional username to instrument-records upload: schema, client formatter, UI selection, users query, backend UsersService validation, and session creation payload.

Changes

Username Threading in Instrument Records Upload

Layer / File(s) Summary
Data Shape
packages/schemas/src/instrument-records/instrument-records.ts
Adds optional username?: string to $UploadInstrumentRecordsData and inferred UploadInstrumentRecordsData.
Query Parameters
apps/web/src/hooks/useUsersQuery.ts
Introduces UsersQueryParams and updates usersQueryOptions / useUsersQuery to accept { params?: { groupId?: string } }, forwards params to axios.get('/v1/users', { params }), and includes params?.groupId in the React Query key.
Formatting & Payload Construction
apps/web/src/utils/upload.ts
reformatInstrumentData gains `currentUsername?: null
UI Integration
apps/web/src/routes/_app/upload/$instrumentId.tsx
Adds selectedUsername state, uses useUsersQuery({ params: { groupId } }), renders a username <Select> (including N/A option), and passes selectedUsername (or currentUser.username) into reformatInstrumentData. Updates upload layout and loading view.
Backend Integration
apps/api/src/instrument-records/instrument-records.service.ts, apps/api/src/instrument-records/instrument-records.module.ts
InstrumentRecordsService.upload now destructures username from payload, injects UsersService, validates provided username via usersService.findByUsername, enforces group membership when groupId is present, and includes username: username ?? undefined when creating retrospective sessions. InstrumentRecordsModule imports UsersModule.
Users Service
apps/api/src/users/users.service.ts
updateById now updates groups using set for mapped groupIds instead of connect.
Tests
apps/api/src/instrument-records/__tests__/instrument-records.service.spec.ts
Test module wiring updated to provide a mock UsersService; new upload tests cover username present, unknown, and omitted scenarios and assert session creation behavior.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant UI as Upload Route (web)
    participant Hook as useUsersQuery
    participant API as Web API
    participant Backend as InstrumentRecordsService
    participant Users as UsersService
    participant Sessions as SessionsService

    User->>UI: open upload, select username & CSV
    UI->>Hook: fetch users (groupId)
    Hook->>API: GET /v1/users?groupId=...
    API-->>Hook: users list
    Hook-->>UI: users loaded
    User->>UI: submit upload
    UI->>API: POST /v1/instrument-records/upload (payload includes username)
    API->>Backend: upload(payload with username)
    Backend->>Users: findByUsername(username) // validate if provided
    Users-->>Backend: user found / not found
    Backend->>Sessions: create({ type: "RETROSPECTIVE", username })
    Sessions-->>Backend: session created
    Backend-->>API: upload complete
    API-->>UI: response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • joshunrau
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a username dropdown to the upload form, which is the primary user-facing feature across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@david-roper david-roper marked this pull request as ready for review May 5, 2026 15:31
@david-roper david-roper requested a review from joshunrau as a code owner May 5, 2026 15:31
@gdevenyi gdevenyi requested a review from Copilot May 5, 2026 15:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to ensure uploaded instrument-record CSV data is associated with a username for audit/traceability, addressing issue #1342 by extending the upload payload and propagating a username through the upload flow into created sessions.

Changes:

  • Extend the upload schema/payload to optionally include a username.
  • Update the web upload flow to send a username alongside uploaded records and add UI to choose the username.
  • Update the API upload service to pass the provided username into session creation.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/schemas/src/instrument-records/instrument-records.ts Adds optional username to the upload payload schema.
apps/web/src/utils/upload.ts Adds currentUsername input and sends username in upload payload.
apps/web/src/routes/_app/upload/$instrumentId.tsx Adds a username selection UI and wires it into upload submission.
apps/web/src/hooks/useUsersQuery.ts Adds optional groupId query param support for fetching users.
apps/api/src/instrument-records/instrument-records.service.ts Passes username into session creation during upload.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

})
)
),
username: z.string().optional()
Comment thread apps/web/src/utils/upload.ts Outdated
Comment thread apps/web/src/hooks/useUsersQuery.ts Outdated
Comment on lines +28 to +32
const groupUsers = useUsersQuery({
params: {
groupId: currentGroup?.id
}
});
Comment thread apps/web/src/routes/_app/upload/$instrumentId.tsx Outdated
Comment on lines +375 to +376
type: 'RETROSPECTIVE',
username: username ?? undefined
Comment thread apps/api/src/instrument-records/instrument-records.service.ts
Copy link
Copy Markdown
Contributor

@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)
apps/web/src/utils/upload.ts (1)

1026-1031: ⚡ Quick win

Send undefined instead of 'N/A' — the backend does an unnecessary findFirst on the literal string.

username: currentUsername ?? 'N/A' always sends a non-empty string. The backend's sessionsService.create then hits the DB with { where: { username: 'N/A' } } for every record in the batch, finds nothing, and sets user = null. Since $UploadInstrumentRecordsData declares username as .optional(), sending undefined (or omitting the field) achieves the same null-user outcome with no DB round-trip.

♻️ Proposed fix
  const reformatForSending: UploadInstrumentRecordsData = {
    groupId: currentGroup?.id,
    instrumentId: instrument.id,
    records: recordsList,
-   username: currentUsername ?? 'N/A'
+   username: currentUsername ?? undefined
  };

And in $instrumentId.tsx the 'N/A' Select.Item value can remain for display purposes — just map it back to undefined before passing to reformatInstrumentData:

- currentUsername: selectedUsername ?? currentUser?.username,
+ currentUsername: (selectedUsername === 'N/A' ? undefined : selectedUsername) ?? currentUser?.username,
🤖 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 `@apps/web/src/utils/upload.ts` around lines 1026 - 1031, reformatForSending is
currently sending the literal 'N/A' for missing users which causes unnecessary
DB lookups; change the assignment in the UploadInstrumentRecordsData object
(reformatForSending.username) to use undefined when currentUsername is not set
(e.g., currentUsername ?? undefined) so the field is omitted/optional, and in
the UI where the Select.Item displays 'N/A' (e.g., in $instrumentId.tsx) map the
display value 'N/A' back to undefined before calling reformatInstrumentData or
constructing reformatForSending so the backend receives no username instead of
the string 'N/A'.
🤖 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 `@apps/web/src/hooks/useUsersQuery.ts`:
- Around line 11-18: usersQueryOptions builds a query whose cache key is
constant (queryKey: [USERS_QUERY_KEY]) so different params (e.g., groupId)
collide; update usersQueryOptions to include the params in the queryKey (for
example use queryKey: [USERS_QUERY_KEY, params ?? null] or a stable
serialization of params) so React Query separates caches per params, and ensure
the same symbol names (usersQueryOptions, params, USERS_QUERY_KEY, queryKey) are
used when modifying the function.

In `@apps/web/src/routes/_app/upload/`$instrumentId.tsx:
- Around line 171-187: The Select can display currentUser?.username even when
that username isn't in groupUsers.data, causing mismatch between shown value and
selectable items; update initialization and/or options: ensure selectedUsername
is pre-seeded to currentUser?.username when the component mounts (or when
currentUser becomes available) and/or add a Select.Item for currentUser if
currentUser.username is not present in groupUsers.data; adjust logic around
selectedUsername, currentUser and groupUsers.data and the Select rendering
(references: selectedUsername, setSelectedUsername, currentUser,
groupUsers.data, Select.Item) so the displayed value always matches an available
option.

---

Nitpick comments:
In `@apps/web/src/utils/upload.ts`:
- Around line 1026-1031: reformatForSending is currently sending the literal
'N/A' for missing users which causes unnecessary DB lookups; change the
assignment in the UploadInstrumentRecordsData object
(reformatForSending.username) to use undefined when currentUsername is not set
(e.g., currentUsername ?? undefined) so the field is omitted/optional, and in
the UI where the Select.Item displays 'N/A' (e.g., in $instrumentId.tsx) map the
display value 'N/A' back to undefined before calling reformatInstrumentData or
constructing reformatForSending so the backend receives no username instead of
the string 'N/A'.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2a1afdfd-ab16-4f51-b58e-c5e8aeedcf4b

📥 Commits

Reviewing files that changed from the base of the PR and between bc0b156 and db5017c.

📒 Files selected for processing (5)
  • apps/api/src/instrument-records/instrument-records.service.ts
  • apps/web/src/hooks/useUsersQuery.ts
  • apps/web/src/routes/_app/upload/$instrumentId.tsx
  • apps/web/src/utils/upload.ts
  • packages/schemas/src/instrument-records/instrument-records.ts

Comment thread apps/web/src/hooks/useUsersQuery.ts
Comment on lines +171 to +187
<Select value={selectedUsername ?? currentUser?.username} onValueChange={setSelectedUsername}>
<Select.Trigger>
<Select.Value placeholder={currentUser?.username} />
</Select.Trigger>
<Select.Content>
<Select.Group>
{groupUsers.data.map((user) => (
<Select.Item key={user.username} value={user.username}>
{user.username}
</Select.Item>
))}
<Select.Item key={'N/A'} value={'N/A'}>
{'N/A'}
</Select.Item>
</Select.Group>
</Select.Content>
</Select>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

If currentUser is not a member of currentGroup, the Select shows a value (currentUser?.username) that has no matching Select.Item.

When selectedUsername is undefined (initial state) and currentUser is not in groupUsers.data, most Radix-based Select implementations will show the placeholder instead of the value — so the displayed username and the submitted username may appear to diverge. Consider pre-seeding selectedUsername from currentUser?.username at initialization, or adding currentUser explicitly to the options list if they could be absent.

🤖 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 `@apps/web/src/routes/_app/upload/`$instrumentId.tsx around lines 171 - 187,
The Select can display currentUser?.username even when that username isn't in
groupUsers.data, causing mismatch between shown value and selectable items;
update initialization and/or options: ensure selectedUsername is pre-seeded to
currentUser?.username when the component mounts (or when currentUser becomes
available) and/or add a Select.Item for currentUser if currentUser.username is
not present in groupUsers.data; adjust logic around selectedUsername,
currentUser and groupUsers.data and the Select rendering (references:
selectedUsername, setSelectedUsername, currentUser, groupUsers.data,
Select.Item) so the displayed value always matches an available option.

david-roper and others added 2 commits May 5, 2026 13:16
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@david-roper david-roper changed the title Username upload always Attach username to upload dropdown May 6, 2026
Copy link
Copy Markdown
Contributor

@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: 1

🧹 Nitpick comments (2)
apps/api/src/instrument-records/__tests__/instrument-records.service.spec.ts (1)

81-140: ⚡ Quick win

Tests cover the happy/sad paths; consider adding a cross-group case.

The three scenarios (valid, unknown, omitted) look right and assert the correct interactions. If you adopt the group-membership check from the service review, please add a test that supplies a username belonging to a different group than groupId and asserts rejection plus no session creation. Also worth tightening: in the unknown-username test, additionally assert subjectsService.createMany was not called, to lock in the "validate before any side effects" ordering.

♻️ Suggested extra assertion
       await expect(instrumentRecordsService.upload({ ...baseUploadData, username: 'spoofed' })).rejects.toBeInstanceOf(
         NotFoundException
       );

+      expect(subjectsService.createMany).not.toHaveBeenCalled();
       expect(sessionsService.create).not.toHaveBeenCalled();
🤖 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 `@apps/api/src/instrument-records/__tests__/instrument-records.service.spec.ts`
around lines 81 - 140, Add a test that verifies upload rejects when the provided
username exists but belongs to a different group than the upload's groupId: mock
usersService.findByUsername to return a user whose groupId !==
baseUploadData.groupId, call instrumentRecordsService.upload({
...baseUploadData, username: 'otherGroupUser' }), expect it to reject (e.g.,
toBeInstanceOf or matching error) and assert sessionsService.create was not
called; also tighten the existing "unknown username" test to assert
subjectsService.createMany was not called to ensure validation happens before
side effects (refer to instrumentRecordsService.upload,
usersService.findByUsername, sessionsService.create, and
subjectsService.createMany).
apps/api/src/instrument-records/instrument-records.service.ts (1)

334-352: 💤 Low value

Consider validating username before the SERIES check, or document the order.

Minor: the SERIES rejection happens before username validation, so an invalid-instrument upload won't surface a stale "user not found" error — that's fine. But note that instrumentsService.findById runs even when username is bogus; if you'd prefer fail-fast on bad usernames (cheaper DB call, clearer error precedence), move the if (username) block above the instrument fetch. Not a blocker.

🤖 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 `@apps/api/src/instrument-records/instrument-records.service.ts` around lines
334 - 352, The upload method currently fetches the instrument and checks
instrumentsService.findById(...)/instrument.kind === 'SERIES' before validating
the username; to prefer fail-fast username validation, move the username
existence check (the if (username) { await
this.usersService.findByUsername(username, options); } call) to run before
calling instrumentsService.findById and before the SERIES rejection, so a
missing or invalid user returns first; alternatively, if you want to preserve
current behavior, add a comment in upload documenting that instrument kind is
validated prior to username lookup to make the order explicit.
🤖 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 `@apps/api/src/instrument-records/instrument-records.service.ts`:
- Around line 349-351: The code calls this.usersService.findByUsername(username,
options) but never verifies the returned user's group membership against the
upload target groupId, allowing a GROUP_MANAGER who spans multiple groups to
attach users from the wrong group; after resolving the user (the result of
this.usersService.findByUsername call) check that the user's groups array
includes the upload's target groupId (the groupId variable used in the upload
flow) and if not, throw a permission/validation error (e.g., ForbiddenException
or a clear ValidationError) so only users belonging to the target group can be
attached; update the upload/attach path in InstrumentRecordsService (the method
around where username is checked) to perform this membership validation using
the resolvedUser.groups before proceeding.

---

Nitpick comments:
In
`@apps/api/src/instrument-records/__tests__/instrument-records.service.spec.ts`:
- Around line 81-140: Add a test that verifies upload rejects when the provided
username exists but belongs to a different group than the upload's groupId: mock
usersService.findByUsername to return a user whose groupId !==
baseUploadData.groupId, call instrumentRecordsService.upload({
...baseUploadData, username: 'otherGroupUser' }), expect it to reject (e.g.,
toBeInstanceOf or matching error) and assert sessionsService.create was not
called; also tighten the existing "unknown username" test to assert
subjectsService.createMany was not called to ensure validation happens before
side effects (refer to instrumentRecordsService.upload,
usersService.findByUsername, sessionsService.create, and
subjectsService.createMany).

In `@apps/api/src/instrument-records/instrument-records.service.ts`:
- Around line 334-352: The upload method currently fetches the instrument and
checks instrumentsService.findById(...)/instrument.kind === 'SERIES' before
validating the username; to prefer fail-fast username validation, move the
username existence check (the if (username) { await
this.usersService.findByUsername(username, options); } call) to run before
calling instrumentsService.findById and before the SERIES rejection, so a
missing or invalid user returns first; alternatively, if you want to preserve
current behavior, add a comment in upload documenting that instrument kind is
validated prior to username lookup to make the order explicit.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1cf19243-ddb2-4abe-90d4-4c1ff2b935b4

📥 Commits

Reviewing files that changed from the base of the PR and between 35b9183 and ad00c8e.

📒 Files selected for processing (5)
  • apps/api/src/instrument-records/__tests__/instrument-records.service.spec.ts
  • apps/api/src/instrument-records/instrument-records.module.ts
  • apps/api/src/instrument-records/instrument-records.service.ts
  • apps/web/src/routes/_app/upload/$instrumentId.tsx
  • apps/web/src/utils/upload.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/routes/_app/upload/$instrumentId.tsx

Comment thread apps/api/src/instrument-records/instrument-records.service.ts
Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (1)
apps/web/src/routes/_app/dashboard.tsx (1)

31-35: ⚡ Quick win

Guard the users query when currentGroup is not ready.

Right now the query can run with groupId undefined. Add an enabled guard so this path never makes an unscoped request.

Proposed change
   const userInfoQuery = useUsersQuery({
+    enabled: Boolean(currentGroup?.id),
     params: {
       groupId: currentGroup?.id
     }
   });
🤖 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 `@apps/web/src/routes/_app/dashboard.tsx` around lines 31 - 35, The users query
(userInfoQuery using useUsersQuery) can run with an undefined groupId and make
an unscoped request; update the call to useUsersQuery to include an enabled
guard (e.g., enabled: !!currentGroup?.id or Boolean(currentGroup?.id)) so the
query only runs when currentGroup.id is available, keeping params.groupId as-is
and ensuring the request is not sent when currentGroup is not ready.
🤖 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.

Nitpick comments:
In `@apps/web/src/routes/_app/dashboard.tsx`:
- Around line 31-35: The users query (userInfoQuery using useUsersQuery) can run
with an undefined groupId and make an unscoped request; update the call to
useUsersQuery to include an enabled guard (e.g., enabled: !!currentGroup?.id or
Boolean(currentGroup?.id)) so the query only runs when currentGroup.id is
available, keeping params.groupId as-is and ensuring the request is not sent
when currentGroup is not ready.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9d3e9a8f-4e7c-4193-a34c-460cf6c109f1

📥 Commits

Reviewing files that changed from the base of the PR and between 31450ac and 6958b82.

📒 Files selected for processing (1)
  • apps/web/src/routes/_app/dashboard.tsx

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.

2 participants