Skip to content

feat(web): add dedicated Remote Assignment page (re-land of #1384)#1386

Merged
joshunrau merged 8 commits into
mainfrom
feature/remote-assignment-page-v2
Jul 3, 2026
Merged

feat(web): add dedicated Remote Assignment page (re-land of #1384)#1386
joshunrau merged 8 commits into
mainfrom
feature/remote-assignment-page-v2

Conversation

@thomasbeaudry

@thomasbeaudry thomasbeaudry commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Re-lands #1384, which was reverted in #1385, with review feedback addressed.

What this does

  • Adds a dedicated Remote Assignment page at /session/remote-assignment for creating remote assignments (choose an instrument, set an expiry, get a shareable link / QR code).
  • Removes the inline "Add assignment" Dialog/Form from datahub/$subjectId/assignments.tsx. Assignment creation now lives solely on the dedicated page; the datahub assignments tab now only displays existing assignments (view / cancel).
  • Adds e2e coverage (2.4-remote-assignment.spec.ts + page object).

Gateway gating (corrected from the original PR description)

The original #1384 description claimed this did not replace the existing creation flow and that the datahub flow "continues to work as before" — that was inaccurate and contributed to the revert. To be precise:

When isGatewayEnabled is false, there is no assignment UI of any kind — same behavior as before this feature. Both entry points are gated:

  • Remote Assignment nav item — useNavItems.ts (behind isGatewayEnabled)
  • Datahub "Assignments" tab — datahub/$subjectId/route.tsx (behind config.setup.isGatewayEnabled)

This is intentional and confirmed: server-side create() always goes through gatewayService.createRemoteAssignment, so assignment creation already required the gateway.

Changes since #1384

  • Removed an unused, broken resultSlider locator from the e2e page object (the AssignmentResultSlider Radix Sheet portals to document.body, so the relative selector never matched and it was never referenced).

Test plan

  • With gateway enabled: Remote Assignment nav item and datahub Assignments tab are visible; can create an assignment from the dedicated page and view/cancel it from the datahub tab.
  • With gateway disabled: neither the nav item nor the datahub Assignments tab appears; no path to create or view assignments.

Refs #1317

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a Remote Assignment session flow to create assignments from an active session, including instrument selection, an expiration date, and a results panel with an assignment link (copy + QR code).
    • Added a “Remote Assignment” navigation link when available.
  • Bug Fixes
    • Improved assignment creation reliability by validating the server response before using it.
    • Updated the Datahub assignments page to focus on viewing/canceling existing assignments.
  • Tests
    • Added end-to-end coverage for the remote assignment flow, including URL/link assertions.

thomasbeaudry and others added 2 commits June 25, 2026 13:06
…page

The resultSlider locator was never referenced in the e2e suite and used a
broken selector — AssignmentResultSlider renders as a Radix Sheet that portals
its content to document.body, so the `~ div [role="dialog"]` selector relative
to the page testid never matched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thomasbeaudry thomasbeaudry requested a review from joshunrau as a code owner June 25, 2026 17:16
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a8d8d9b3-8410-4b83-877f-7795b58fa4b7

📥 Commits

Reviewing files that changed from the base of the PR and between 22039f9 and 6a1058f.

📒 Files selected for processing (1)
  • apps/web/src/routes/_app/session/remote-assignment.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/routes/_app/session/remote-assignment.tsx

Walkthrough

Adds a remote-assignment session route and nav entry, moves assignment creation into that route, removes creation UI from the assignments page, and updates Playwright coverage and supporting translations/setup.

Changes

Remote Assignment Flow

Layer / File(s) Summary
Route registration and navigation
apps/web/src/route-tree.ts, apps/web/src/hooks/useNavItems.ts, apps/web/src/translations/layout.json
Registers /session/remote-assignment, adds the gated session nav item, and adds the navigation label translation.
Remote assignment page and submission
apps/web/src/hooks/useCreateAssignment.ts, apps/web/src/routes/_app/session/remote-assignment.tsx
Adds the remote-assignment route, validates assignment creation responses with $Assignment, and renders the create dialog plus result slider.
Assignments list cleanup
apps/web/src/routes/_app/datahub/$subjectId/assignments.tsx, apps/web/src/translations/datahub.json
Removes the create-assignment UI from the assignments route, refactors the table rendering, and removes the deleted assignment/error translation keys.
E2E route model and coverage
testing/e2e/src/pages/remote-assignment.page.ts, testing/e2e/src/helpers/fixtures.ts, testing/e2e/src/global/global.setup.spec.ts, testing/e2e/src/2.4-remote-assignment.spec.ts
Adds the page object, registers the route in Playwright fixtures, updates E2E setup for accessible instruments, and covers the new remote-assignment flow in an end-to-end spec.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RouteComponent
  participant createAssignmentMutation
  participant axios
  participant AssignmentParse as $Assignment.parse
  participant AssignmentResultSlider
  RouteComponent->>createAssignmentMutation: mutateAsync(payload)
  createAssignmentMutation->>axios: POST /v1/assignments
  axios-->>createAssignmentMutation: response.data
  createAssignmentMutation->>AssignmentParse: parse(response.data)
  AssignmentParse-->>RouteComponent: assignment data
  RouteComponent->>AssignmentResultSlider: show assignment.url
Loading

Possibly related PRs

Suggested reviewers: joshunrau

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a dedicated Remote Assignment page in the web app.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/remote-assignment-page-v2

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.

@thomasbeaudry

Copy link
Copy Markdown
Collaborator Author

@joshunrau this re-lands #1384 with the feedback addressed:

  • Gateway gating — confirmed isGatewayEnabled === false hides all assignment UI (nav item + datahub Assignments tab), matching the "same as before" behavior we discussed. No code change was needed; the existing gates already do this.
  • Dead e2e locator — removed the unused/broken resultSlider locator you flagged.
  • PR description — rewritten to accurately state the datahub create flow was removed and creation now lives on the dedicated page (the original description was the misleading part).

Ready for another look when you have a sec.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

🤖 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/routes/_app/datahub/`$subjectId/assignments.tsx:
- Line 118: The Table.Cell expression in the assignments view is hitting the
same unsafe member access on details.title as the remote-assignment page. Fix
the underlying instrument type resolution in the data lookup used by this
component so instruments[assignment.instrumentId] is typed with a safe details
shape, and then keep the title fallback logic in place without any unsafe member
access.

In `@apps/web/src/routes/_app/session/remote-assignment.tsx`:
- Line 140: The `selectedInstrument?.details.title` access in
`remote-assignment.tsx` is being flagged as unsafe by TypeScript/lint. Update
the expression around `selectedInstrument` to either add the requested
`@ts-expect-error` suppression directly above the unsafe member access or
refactor the type assertion/cast so `details` is treated as a safe typed
property before reading `title`.
- Around line 168-176: The onSubmit handler in remote-assignment.tsx is using a
non-null assertion on currentSession.subject, which can crash when the Session
subject is null. Update the createAssignmentMutation.mutateAsync call to avoid
currentSession.subject! and instead use the stable subject reference already
available via currentSession.subjectId, or add an explicit guard before
submitting if a subject is required. Keep the fix localized to the onSubmit flow
around createAssignmentMutation and currentSession.

In `@testing/e2e/src/pages/remote-assignment.page.ts`:
- Around line 23-27: The submit locator in submitAssignmentForm is too brittle
because getByLabel('Submit') is intended for labeled inputs, not buttons. Update
the locator in remote-assignment.page.ts to use getByRole('button', { name:
'Submit' }) on createDialog so the Playwright page object targets the submit
action by accessible role and name, keeping the method reliable if the Form
component’s markup changes.
🪄 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: ac934e56-6564-4ee3-8363-ff4d4d327a2f

📥 Commits

Reviewing files that changed from the base of the PR and between 6cf7293 and 67d97a8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • apps/web/src/hooks/useCreateAssignment.ts
  • apps/web/src/hooks/useNavItems.ts
  • apps/web/src/route-tree.ts
  • apps/web/src/routes/_app/datahub/$subjectId/assignments.tsx
  • apps/web/src/routes/_app/session/remote-assignment.tsx
  • apps/web/src/translations/datahub.json
  • apps/web/src/translations/layout.json
  • testing/e2e/src/2.4-remote-assignment.spec.ts
  • testing/e2e/src/helpers/fixtures.ts
  • testing/e2e/src/pages/remote-assignment.page.ts
💤 Files with no reviewable changes (1)
  • apps/web/src/translations/datahub.json

key={assignment.id}
onClick={() => onSelection(assignment)}
>
<Table.Cell>{instruments[assignment.instrumentId]?.details.title ?? assignment.instrumentId}</Table.Cell>

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Same unsafe .title ESLint error as the remote-assignment page.

instruments[assignment.instrumentId]?.details.title triggers the same no-unsafe-member-access [error]. Likely the same UnilingualInstrumentInfo type-resolution root cause; fixing the type should clear both.

🧰 Tools
🪛 ESLint

[error] 118-118: Unsafe member access .title on a type that cannot be resolved.

(@typescript-eslint/no-unsafe-member-access)

🤖 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/datahub/`$subjectId/assignments.tsx at line 118, The
Table.Cell expression in the assignments view is hitting the same unsafe member
access on details.title as the remote-assignment page. Fix the underlying
instrument type resolution in the data lookup used by this component so
instruments[assignment.instrumentId] is typed with a safe details shape, and
then keep the title fallback logic in place without any unsafe member access.

Source: Linters/SAST tools

en: 'Assign "{}" to the current subject for remote completion.',
fr: 'Assigner « {} » au client actuel pour complétion à distance.'
},
{ args: [selectedInstrument?.details.title ?? ''] }

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.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP "TranslatedInstrumentInfo" packages/schemas/src -C2
fd -t f 'eslint.config.*' apps/web
rg -nP "no-unsafe-member-access|recommendedTypeChecked|lint" apps/web/package.json

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 1016


Fix unsafe access to details.title.

TranslatedInstrumentInfo is correctly imported, but TypeScript resolves the .details property as unsafe (likely unknown or any) in this context, triggering the no-unsafe-member-access error. Add a @ts-expect-error disable directive or refactor the type cast to silence the error.

Snippet
// `@ts-expect-error` Unsafe member access on `details`
{ args: [selectedInstrument?.details.title ?? ''] }
🧰 Tools
🪛 ESLint

[error] 140-140: Unsafe member access .title on a type that cannot be resolved.

(@typescript-eslint/no-unsafe-member-access)

🤖 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/session/remote-assignment.tsx` at line 140, The
`selectedInstrument?.details.title` access in `remote-assignment.tsx` is being
flagged as unsafe by TypeScript/lint. Update the expression around
`selectedInstrument` to either add the requested `@ts-expect-error` suppression
directly above the unsafe member access or refactor the type assertion/cast so
`details` is treated as a safe typed property before reading `title`.

Source: Linters/SAST tools

Comment thread apps/web/src/routes/_app/session/remote-assignment.tsx
Comment on lines +23 to +27
async submitAssignmentForm() {
const submitButton = this.createDialog.getByLabel('Submit');
await submitButton.waitFor({ state: 'visible' });
await submitButton.click();
}

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.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the Form component and inspect how the submit button is rendered.
fd -t f -e tsx -e ts 'Form' apps/web/src 2>/dev/null
rg -nP -C3 'type=["'\'']submit["'\'']|getByRole\(.?button|aria-label' --type=ts --type=tsx apps/web/src | rg -i submit -C2
# Also check other e2e page objects for the prevailing submit-button locator convention.
rg -nP -C2 "getBy(Label|Role)\(" testing/e2e/src/pages

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 3381


🏁 Script executed:

cat apps/web/src/components/Shared/Form.tsx 2>/dev/null || cat apps/web/src/components/Form/Form.tsx 2>/dev/null || fd -t f -e tsx 'Form.tsx'  | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && cat "{}"'

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 17089


🏁 Script executed:

fd -t f -e tsx -e ts 'index.tsx' | xargs grep -l "submitBtnLabel" 2>/dev/null | head -5

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 224


🏁 Script executed:

find . -path "*/node_modules" -prune -o -type f -name "*.tsx" -exec grep -l "export.*Form.*props" {} \; -quit | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && cat "{}"'

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 177


🏁 Script executed:

grep -r "submitBtnLabel" --include="*.tsx" --include="*.ts" -A 10 | grep -E "(aria-label|label=|type=[\"']submit)" | head -20

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 177


🏁 Script executed:

cat node_modules/@douglasneuroinformatics/libui/dist/components/form/Form.js 2>/dev/null || grep -r "submitBtnLabel" --include="*.tsx" --include="*.ts" node_modules/@douglasneuroinformatics/libui 2>/dev/null | head -20

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 177


🏁 Script executed:

grep -r "Form" --include="*.tsx" --include="*.ts" apps/web/src/libui 2>/dev/null | head -5

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 177


🏁 Script executed:

ls -la apps/web/src/ | grep -E "lib|ui"

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 177


🏁 Script executed:

cat apps/web/src/routes/_app/session/remote-assignment/page.tsx 2>/dev/null

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 177


🏁 Script executed:

grep -r "submitBtnLabel" --include="*.tsx" --include="*.ts" --include="*.json" apps/ web/ 2>/dev/null -B 2 -A 5 | head -50

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 4650


🏁 Script executed:

fd -e tsx -e ts 'libui' | head -10

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 177


🏁 Script executed:

grep -r "<Form" --include="*.tsx" apps/web/src/routes/_app/session/remote-assignment* 2>/dev/null -B 5 -A 10

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 628


🏁 Script executed:

fd -t f -e tsx 'page.tsx' apps/web/src/routes/_app/session/remote-assignment* 2>/dev/null | xargs cat

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 177


🏁 Script executed:

grep -r "class Form" --include="*.tsx" --include="*.ts" . | grep -v node_modules | head -10

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 292


🏁 Script executed:

cat apps/web/src/components/IdentificationForm/IdentificationForm.page.tsx 2>/dev/null || fd -e tsx -e ts 'remote-assignment' | xargs -I {} grep -l "Form" {}

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 326


🏁 Script executed:

rg -A 50 "export default function RemoteAssignmentPage" apps/web/src/routes/_app/session/remote-assignment/page.tsx | rg -A 50 "Form"

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 277


🏁 Script executed:

rg "submitBtnLabel" --type tsx --type ts -B 1 -A 1 | rg -A 5 -B 5 "submit.*btn"

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 205


🏁 Script executed:

rg "aria-label.*submit" --type tsx --type ts -i | head -20

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 205


🏁 Script executed:

rg "type.*submit" --type tsx --type ts -B 5 -A 5 | rg -B 5 -A 5 "label" | head -40

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 205


🏁 Script executed:

cat apps/web/src/routes/_app/session/remote-assignment.tsx

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 7639


🏁 Script executed:

python3 - <<'PY'
import json
import re

# Based on the code, the Form component is from '`@douglasneuroinformatics/libui/components`'
# It receives a 'content' object and 'submitBtnLabel' prop (optional).
# If 'submitBtnLabel' is not provided, it likely defaults to something.
# Let's check if there are any specific type definitions or if we can infer the DOM structure.
# The test uses `getByLabel('Submit')`.
# In the code `remote-assignment.tsx`, the Form has `content` and `onSubmit` but *no* `submitBtnLabel`.
# If the library's default submitBtnLabel is 'Submit', then `getByLabel('Submit')` might work IF:
# 1. The button has an `aria-label="Submit"`
# 2. The button text is "Submit" (GetByLabel works on text for buttons? No. GetByLabel is for inputs with <label> or aria-label. GetByText is for button text.)
#
# If the library wraps the button in a <label> tag, that would be weird.
# Or perhaps the button has `id="submit"` and the label is hidden?
#
# Key finding: The test code uses `getByLabel` on a `Dialog`.
# `getByLabel` usually relies on `aria-label` or a `<label for="...">`.
# A submit button in a form usually has text content ("Submit") or `aria-label`.
#
# Let's check the other forms again.
# `StartSessionForm` has `submitBtnLabel={t('core.submit')}`.
# `remote-assignment.tsx` does NOT provide `submitBtnLabel`.
# Does the libui `Form` component provide a default? Likely.
#
# However, `getByLabel` is technically the WRONG semantic selector for a button.
# `getByRole('button', { name: 'Submit' })` is the robust choice.
# Even if `getByLabel` works now, it relies on implementation details (e.g., if the library adds an aria-label for some reason).
# If the library changes and stops adding aria-label, the test breaks.
#
# Conclusion: The warning in the original review is valid advice for robustness.
# The implementation detail (whether the button currently has aria-label) is secondary to the architectural recommendation of using the correct ARIA role selector.
# But I need to confirm if the specific instance in the code is actually broken or just bad practice.
# In `remote-assignment.tsx`, no `submitBtnLabel` is passed.
# If the default is 'Submit', and the button has text "Submit", `getByRole` works.
# Does `getByLabel` work on text? No, unless it acts like a text value.
#
# Let's assume the reviewer is correct about the risk.
# The code in `remote-assignment.page.ts` line 24 uses `getByLabel`.
# I will recommend switching to `getByRole`.

print("Analysis complete: Using getByRole is the idiomatic and robust approach for buttons.")
PY

Repository: DouglasNeuroInformatics/OpenDataCapture

Length of output: 262


Replace getByLabel('Submit') with getByRole('button', { name: 'Submit' }) for better reliability.

getByLabel targets inputs with associated <label> or aria-label attributes. Submit buttons are best targeted by their role (button) and accessible name. This change ensures the locator remains valid even if the underlying Form component changes how it renders the button label.

Snippet
  async submitAssignmentForm() {
    const submitButton = this.createDialog.getByRole('button', { name: 'Submit' });
    await submitButton.waitFor({ state: 'visible' });
    await submitButton.click();
  }
🤖 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 `@testing/e2e/src/pages/remote-assignment.page.ts` around lines 23 - 27, The
submit locator in submitAssignmentForm is too brittle because
getByLabel('Submit') is intended for labeled inputs, not buttons. Update the
locator in remote-assignment.page.ts to use getByRole('button', { name: 'Submit'
}) on createDialog so the Playwright page object targets the submit action by
accessible role and name, keeping the method reliable if the Form component’s
markup changes.

thomasbeaudry and others added 6 commits June 25, 2026 13:50
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t setup

The 2.4-remote-assignment E2E test timed out waiting for an instrument
card to appear. The per-browser groups created in global setup had empty
accessibleInstrumentIds, so the group-scoped user saw no instruments and
the InstrumentShowcase rendered with no <li> to click.

Fetch the instrument list as admin and patch each created group with
those accessibleInstrumentIds so the showcase is populated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Avoid the non-null assertion on the nullable currentSession.subject by
reading subjectId directly off the session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@joshunrau joshunrau merged commit 8e23112 into main Jul 3, 2026
6 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.

2 participants