SF-3768 Add assignee and resolution to draft request detail page#3780
SF-3768 Add assignee and resolution to draft request detail page#3780
Conversation
...ClientApp/src/app/serval-administration/onboarding-requests/onboarding-requests.component.ts
Show resolved
Hide resolved
...ClientApp/src/app/serval-administration/onboarding-requests/onboarding-requests.component.ts
Show resolved
Hide resolved
| get availableAssigneeIds(): string[] { | ||
| return this.onboardingRequestService.getCurrentlyAssignedUserIdsFromRequestList(this.requests); | ||
| } |
There was a problem hiding this comment.
🔴 availableAssigneeIds getter creates new array on every access, defeating optimistic UI updates
The availableAssigneeIds getter at onboarding-requests.component.ts:135-137 returns a new array from getCurrentlyAssignedUserIdsFromRequestList() on every call. In the template, it's bound as [knownAssigneeIds]="availableAssigneeIds". Angular's change detection compares by reference, so it sees a "change" on every cycle, triggering ngOnChanges() in OnboardingRequestAssigneeSelectComponent (onboarding-request-assignee-select.component.ts:30-32). That handler resets internalValue back to the parent's value input, immediately undoing the optimistic UI update set in onSelectionChange. The dropdown visually reverts to the old assignee before the API call completes.
Prompt for agents
The availableAssigneeIds getter in OnboardingRequestsComponent creates a new array on every access. When used as a template binding [knownAssigneeIds]="availableAssigneeIds", Angular detects a reference change on every change detection cycle and triggers ngOnChanges in OnboardingRequestAssigneeSelectComponent, which resets internalValue to the parent value, defeating the optimistic UI update.
Possible fixes:
1. Cache the computed array as a class property (e.g. availableAssigneeIds: string[] = []) and recompute it only when this.requests changes (in loadRequests, onAssigneeChange, onResolutionChange) rather than using a getter. This is the approach used in the detail component.
2. Alternatively, make OnboardingRequestAssigneeSelectComponent.ngOnChanges smarter by using SimpleChanges to only reset internalValue when the value input specifically changes, not when knownAssigneeIds changes.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
What might a solution be?
| async onAssigneeChange(newAssigneeId: string): Promise<void> { | ||
| if (this.request == null) return; | ||
| this.request = await this.onboardingRequestService.setAssignee(this.request.id, newAssigneeId); | ||
| } | ||
|
|
||
| async onResolutionChange(newResolution: DraftRequestResolutionKey | null): Promise<void> { | ||
| if (this.request == null) return; | ||
| this.request = await this.onboardingRequestService.setResolution(this.request.id, newResolution); | ||
| } |
There was a problem hiding this comment.
🚩 Detail component's onAssigneeChange/onResolutionChange lack error handling unlike the list component
In DraftRequestDetailComponent, onAssigneeChange (line 126-129) and onResolutionChange (line 131-134) have no try/catch. If the API call fails, the error will be an unhandled promise rejection since these are invoked from the template via (selectionChange). By contrast, OnboardingRequestsComponent wraps the same calls in try/catch with error messages and state recovery (onboarding-requests/onboarding-requests.component.ts:187-203). This inconsistency means failures in the detail view silently fail with no user feedback.
Was this helpful? React with 👍 or 👎 to provide feedback.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3780 +/- ##
==========================================
- Coverage 81.27% 81.12% -0.15%
==========================================
Files 622 624 +2
Lines 39322 39480 +158
Branches 6391 6423 +32
==========================================
+ Hits 31958 32029 +71
- Misses 6379 6460 +81
- Partials 985 991 +6 ☔ View full report in Codecov by Sentry. |
0f418e3 to
51cd3d6
Compare
| async onAssigneeChange(newAssigneeId: string): Promise<void> { | ||
| if (this.request == null) return; | ||
| this.request = await this.onboardingRequestService.setAssignee(this.request.id, newAssigneeId); | ||
| } | ||
|
|
||
| async onResolutionChange(newResolution: DraftRequestResolutionKey | null): Promise<void> { | ||
| if (this.request == null) return; | ||
| this.request = await this.onboardingRequestService.setResolution(this.request.id, newResolution); | ||
| } |
There was a problem hiding this comment.
🔴 Missing error handling in onAssigneeChange/onResolutionChange causes stale optimistic UI on API failure
In DraftRequestDetailComponent, both onAssigneeChange (draft-request-detail.component.ts:126-129) and onResolutionChange (draft-request-detail.component.ts:131-134) lack try/catch error handling, unlike their counterparts in OnboardingRequestsComponent (onboarding-requests.component.ts:187-206 and onboarding-requests.component.ts:212-231) which have proper try/catch/finally.
When the API call fails:
- The
OnboardingRequestAssigneeSelectComponenthas already performed an optimistic update tointernalValue(onboarding-request-assignee-select.component.ts:35), and sincethis.requestis never updated on failure, the parent's[value]input doesn't change,ngOnChangeswon't fire, and the dropdown permanently shows the wrong value. - Similarly, the resolution
mat-selectwill display the user's new selection whilerequest.resolutionremains unchanged. - No error message is shown to the user.
- An unhandled promise rejection occurs since Angular event handlers don't await the returned promise.
Prompt for agents
Both onAssigneeChange and onResolutionChange in DraftRequestDetailComponent need try/catch/finally error handling, following the same pattern used in OnboardingRequestsComponent (onboarding-requests.component.ts:187-206 and 212-231). On error, show a user-facing error notification via this.noticeService.showError(), and reload the request data to restore the correct UI state. This is needed because on failure the OnboardingRequestAssigneeSelectComponent has already optimistically updated its internal display value, and the resolution mat-select has visually changed. Without reverting these, the UI will permanently show incorrect state.
Was this helpful? React with 👍 or 👎 to provide feedback.
| function createTestRequest(overrides: Partial<OnboardingRequest> = {}): OnboardingRequest { | ||
| return { | ||
| id: REQUEST_ID, | ||
| submittedAt: '2024-01-01T00:00:00Z', | ||
| submittedBy: { name: 'Test User', email: 'test@example.com' }, | ||
| submission: { | ||
| projectId: 'project01', | ||
| userId: 'user03', | ||
| timestamp: '2024-01-01T00:00:00Z', | ||
| formData: { | ||
| name: 'Test User', | ||
| email: 'test@example.com', | ||
| organization: 'Test Org', | ||
| partnerOrganization: 'Partner Org', | ||
| translationLanguageName: 'English', | ||
| translationLanguageIsoCode: 'en', | ||
| completedBooks: [40, 41, 42, 43], | ||
| nextBooksToDraft: [44], | ||
| sourceProjectA: 'ptproject01', | ||
| draftingSourceProject: 'ptproject02', | ||
| backTranslationStage: 'None', | ||
| backTranslationProject: null | ||
| } | ||
| }, | ||
| assigneeId: '', | ||
| status: 'new', | ||
| resolution: 'unresolved', | ||
| comments: [], | ||
| ...overrides | ||
| }; | ||
| } |
There was a problem hiding this comment.
🔴 Helper function createTestRequest is defined outside the TestEnvironment class
The createTestRequest function is defined at module level outside both the describe block and the TestEnvironment class. AGENTS.md mandates: "Do not put helper functions outside of TestEnvironment classes; helper functions or setup functions should be in the TestEnvironment class." This function should be a static method on the TestEnvironment class.
Prompt for agents
Move the createTestRequest function into the TestEnvironment class (defined at draft-request-detail.component.spec.ts:149) as a static method. It is currently at module level (line 34-64) but AGENTS.md requires helper functions to be inside the TestEnvironment class. Since it is used both in the TestEnvironment constructor and in individual test cases, make it static createTestRequest(...) and call it as TestEnvironment.createTestRequest(...).
Was this helpful? React with 👍 or 👎 to provide feedback.
| const requests = await this.onboardingRequestService.getAllRequests(); | ||
| if (requests != null) { | ||
| this.requests = requests; | ||
| this.initializeRequestData(); | ||
| this.filterRequests(); | ||
| void this.loadProjectNames(); | ||
| } | ||
| this.loadingFinished(); | ||
| } catch (error) { | ||
| console.error('Error loading draft requests:', error); | ||
| this.noticeService.showError('Failed to load draft requests'); | ||
| } finally { | ||
| this.loadingFinished(); | ||
| } | ||
| } |
There was a problem hiding this comment.
🚩 loadRequests error handling changed from catch-and-notify to finally-only
In onboarding-requests.component.ts:118-130, the old code had a catch block that logged the error and called noticeService.showError('Failed to load draft requests'). The new code uses only try/finally, so errors will propagate as unhandled rejections without a user-facing message. Since this is called via void this.loadRequests() from ngOnInit(), the rejection won't be caught. This is a pre-existing pattern shift—the detail component's loadRequest also uses try/finally without catch. Both are consistent with each other now, but the user loses the error notification.
(Refers to lines 118-130)
Was this helpful? React with 👍 or 👎 to provide feedback.
51cd3d6 to
d81b37a
Compare
This change takes this capability:

...and copies it here, while trying to avoid duplicating logic too much.

This change is