feat(committees): prefill org from profile on join and apply flows - #1337
Conversation
When joining an open committee or applying to join one that requires an organization (voting enabled or business email required), the Confirm Organization dialog was not pre-populated from the user's profile. For invite flows it was already pre-filling from the invite record, but open-join and apply paths had no equivalent source. Extract a public resolveCurrentEmployer() method on InvitationAcceptFlowService that fetches the user's work experiences, picks the current employer, and resolves its domain — mirroring what preResolveOrganization does for invite payloads. Call it from committee-view before opening the organization dialog on both the open-join and apply-to-join paths so the dialog is pre-filled. Tighten the isNewOrg and orgInvalid computed properties in AcceptInviteOrganizationDialogComponent: organization_url is always required because organization_id (CDP UUID) is stripped before the payload reaches committee-service, which expects name + domain. orgInvalid now enforces URL presence regardless of whether CDP resolved an ID. Resolves: LFXV2-2690 Signed-off-by: Andres Tobon <andrest2455@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
There was a problem hiding this comment.
🟡 Changes recommended
URL validation, premature CDP mutation, and route-lifecycle handling need correction.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Prefills committee join/apply organization dialogs from the user profile and enforces organization domains.
Changes:
- Adds current-employer and domain resolution.
- Prefills join/apply dialogs.
- Requires valid organization URLs.
File summaries
| File | Description |
|---|---|
invitation-accept-flow.service.ts |
Resolves profile employer details. |
accept-invite-organization-dialog.component.ts |
Tightens organization validation. |
committee-view.component.ts |
Prefills join and application dialogs. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
- Guard handleJoinRequest against double-tap during async org prefetch: add resolvingOrg signal, check it alongside joiningOrLeaving at entry, and wrap each openOrganizationDialog call with .finally() to clear it - Eliminate duplicated work-experience fetch in accept(): replace the inline GET /api/profile/work-experiences + currentEmployerFromWorkExperiences chain with a call to the newly extracted resolveCurrentEmployer() - Add console.warn to resolveCurrentEmployer() catchError so silent fallbacks are observable in DevTools instead of swallowed silently - Convert effect() in AcceptInviteOrganizationDialogComponent to toObservable() + takeUntilDestroyed() per repo no-effect convention LFXV2-2690 Signed-off-by: Andres Tobon <andrest2455@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
🟡 Changes recommended
Website validation is inadvertently disabled after input, and invite fallback resolution may run twice.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (2)
apps/lfx-one/src/app/shared/components/accept-invite-organization-dialog/accept-invite-organization-dialog.component.ts:52
isNewOrgbecomes false as soon as the user enters any non-empty website value. The subscription below then clearshttpsUrlValidator, makingurlStatusbecomeVALID, so a value such asnot-a-urlenables Confirm and is sent upstream despite the new name-plus-domain requirement. Keep the URL validators active whenever an organization name is present, rather than only while the URL is empty.
const hasUrl = !!(value?.organization_url ?? '').trim();
return !hasUrl && !!value?.organization?.trim();
apps/lfx-one/src/app/shared/services/invitation-accept-flow.service.ts:51
- When the invite has no organization,
resolveCurrentEmployer()already callsresolveOrgDomain(), and the unconditionalpreResolveOrganization()immediately repeats that lookup if the first search misses or times out. This doubles the search/resolve requests and can make the invite dialog wait up to 4 seconds despite the documented 2-second fallback. Pre-resolve only an organization supplied by the invite; the employer fallback is already resolved.
const contextReady$: Observable<InvitationAcceptContext> = context.organization
? of(context)
: this.resolveCurrentEmployer().pipe(map((org) => ({ ...context, organization: org ?? undefined })));
return contextReady$.pipe(
switchMap((ctx) => this.preResolveOrganization(ctx)),
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
…cycle - isNewOrg: keep httpsUrlValidator active whenever org name is present; previous !hasUrl check cleared validators on the first URL keystroke, allowing invalid values like "not-a-url" to pass urlStatus as VALID. - invitation-accept-flow: when no invite org, employer fallback path via resolveCurrentEmployer() already calls resolveOrgDomain() internally; remove the unconditional preResolveOrganization() so the CDP lookup (which can be a find-or-create POST) does not run twice. - committee-view/openOrganizationDialog: pipe resolveCurrentEmployer() through takeUntilDestroyed(destroyRef) before firstValueFrom() so the subscription is canceled if the component is destroyed mid-flight. Signed-off-by: Andres Tobon <andrest2455@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
🟡 Changes recommended
The prefetch can wait indefinitely and can still open a dialog after component destruction.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (2)
apps/lfx-one/src/app/shared/services/invitation-accept-flow.service.ts:84
- The 2-second timeout only exists inside
resolveOrgDomain, so it does not start until/api/profile/work-experiencesemits. A stalled profile request therefore leaves the new join/apply flow waiting indefinitely instead of opening the blank fallback dialog. Apply the timeout afterswitchMapso it covers the entire prefetch and reaches the existingcatchError.
return this.http.get<WorkExperienceEntry[]>('/api/profile/work-experiences').pipe(
take(1),
map((experiences) => currentEmployerFromWorkExperiences(experiences)),
switchMap((org) => (org ? this.resolveOrgDomain(org) : of(null))),
apps/lfx-one/src/app/modules/committees/committee-view/committee-view.component.ts:811
takeUntilDestroyedcompletes the source, butfirstValueFrom(..., { defaultValue: null })then resolves normally, so execution still reachesdialogService.openafter this component has been destroyed. CheckDestroyRef.destroyedafter the await and return before opening the dialog.
const prefillOrg = await firstValueFrom(this.invitationAcceptFlow.resolveCurrentEmployer().pipe(takeUntilDestroyed(this.destroyRef)), {
defaultValue: null,
});
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
firstValueFrom's defaultValue: null resolves successfully even when takeUntilDestroyed completes the observable early due to component teardown, so dialogService.open was still called over the next route. Add an explicit destroyed flag registered via destroyRef.onDestroy() before the await, checked immediately after — if the component was destroyed during the employer prefetch the method returns null instead of opening the Confirm Organization dialog. LFXV2-2690 Signed-off-by: Andres Tobon <andrest2455@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new prefetch can open a stale committee dialog after parameter-only route navigation.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (1)
apps/lfx-one/src/app/modules/committees/committee-view/committee-view.component.ts:824
takeUntilDestroyeddoes not cover navigation from/groups/Ato/groups/B, because Angular reuses this component across:idchanges (as this file notes at lines 180–183). If the route changes during this new prefetch, the await can still open A's dialog over B and keep B's join action blocked. Capture the committee ID before awaiting and return if it changed, or cancel onparamMapchanges.
const prefillOrg = await firstValueFrom(this.invitationAcceptFlow.resolveCurrentEmployer().pipe(takeUntilDestroyed(this.destroyRef)), {
defaultValue: null,
});
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
Resolved conflict in committee-view.component.ts: kept both firstValueFrom (from this branch) and Observable (from main) in the rxjs import list. Signed-off-by: Andres Tobon <andrest2455@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bbde395. Configure here.
- Add resolvingOrg() to Join Group and Apply to Join button [loading] bindings so the CTA shows a spinner during the employer prefetch - Add timeout(2000) to the work-experiences GET in resolveCurrentEmployer so a hung profile call does not block the join flow indefinitely LFXV2-2690 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
Suppressed comments (3)
apps/lfx-one/src/app/modules/committees/committee-view/committee-view.component.ts:202
- The comment says
resolvingOrgonly blocks during the async org-prefetch window, but it's set for the entireopenOrganizationDialog()lifetime (including while the dialog is open), since.finally(...)runs after the dialog promise resolves. Either update the comment to match behavior, or move theresolvingOrgtoggling insideopenOrganizationDialog()to only wrap the prefetch portion.
// Blocks the join/apply CTA during the async org-prefetch window so a second tap
// doesn't fire a parallel resolveCurrentEmployer() + dialog pair.
apps/lfx-one/src/app/modules/committees/committee-view/committee-view.component.ts:541
- The comment says
resolvingOrgonly blocks during the async org-prefetch window, but it's set for the entireopenOrganizationDialog()lifetime (including while the dialog is open), since.finally(...)runs after the dialog promise resolves. Either update the comment to match behavior, or move theresolvingOrgtoggling insideopenOrganizationDialog()to only wrap the prefetch portion.
this.resolvingOrg.set(true);
const result = await this.openOrganizationDialog(committee.name).finally(() => this.resolvingOrg.set(false));
apps/lfx-one/src/app/shared/components/accept-invite-organization-dialog/accept-invite-organization-dialog.component.ts:52
isNewOrgno longer indicates whether the org is 'new' (it now just checks for a non-empty organization name). This name is misleading and makes validation logic harder to follow. Rename it to reflect intent (e.g.,hasOrgName,requiresUrlValidation, or similar) and update references.
protected readonly isNewOrg = computed(() => {
const value = this.formValue();
return !!value?.organization?.trim();
});
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
apps/lfx-one/src/app/shared/services/invitation-accept-flow.service.ts:119
- This still performs a find-or-create POST during prefill:
resolveOrgDomain()reachesOrganizationService.resolveOrganization(), whose server path creates a CDP organization when the searched domain is absent. The new open-join/apply callers therefore mutate CDP merely by clicking the CTA, even if the user later cancels. The exact search match already supplies the domain needed for prefill; defer resolution/creation to the dialog's confirm path.
...org,
id: resolved.id || null,
name: resolved.name || org.name,
website: normalizeToUrl(match.domain) ?? org.website,
}))
Angular strict template type-checking disallows private members in templates; resolvingOrg is bound in [loading] so it must be at least protected. LFXV2-2690 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
After awaiting openOrganizationDialog (which includes up to 2s of employer prefetch), check that committeeId() still matches the committee captured at call-start. Angular reuses the committee-view component for in-app navigation between /groups/:id routes without destroying it, so DestroyRef never fires; without this check a prefetch started on committee A could open a dialog whose confirmation submits a join/apply to committee A even though the user has already navigated to committee B. Added the committeeId() !== committee.uid guard in both the open and application join paths, immediately after the dialog resolves. LFXV2-2690 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
apps/lfx-one/src/app/shared/services/invitation-accept-flow.service.ts:108
- The search query uses
org.namewithout trimming, but the match comparison trims the org name. Iforg.namecontains leading/trailing whitespace (plausible from profile data/manual entry),searchOrganizations()may miss suggestions and skip domain resolution unnecessarily. Trim once into a localorgName = org.name.trim()and use it for both the search query and the exact-match comparison.
private resolveOrgDomain(org: CommitteeOrganizationReference): Observable<CommitteeOrganizationReference> {
if (!org?.name?.trim() || (org.id && org.website?.trim())) {
return of(org);
}
return this.organizationService.searchOrganizations(org.name!).pipe(
take(1),
switchMap((suggestions) => {
const match = suggestions.find((s) => s.name.toLowerCase() === org.name!.toLowerCase().trim());
apps/lfx-one/src/app/shared/components/accept-invite-organization-dialog/accept-invite-organization-dialog.component.ts:52
isNewOrgno longer reflects whether the org is 'new' (it’s now true whenever an organization name is present). This is misleading for future readers since it drives URL validator behavior. Rename it to something that matches the new semantics (e.g.,hasOrganizationNameorshouldValidateOrganizationUrl) and update references accordingly.
protected readonly isNewOrg = computed(() => {
const value = this.formValue();
return !!value?.organization?.trim();
});
apps/lfx-one/src/app/shared/components/accept-invite-organization-dialog/accept-invite-organization-dialog.component.ts:110
isNewOrgno longer reflects whether the org is 'new' (it’s now true whenever an organization name is present). This is misleading for future readers since it drives URL validator behavior. Rename it to something that matches the new semantics (e.g.,hasOrganizationNameorshouldValidateOrganizationUrl) and update references accordingly.
toObservable(this.isNewOrg)
.pipe(takeUntilDestroyed())
.subscribe((isNew) => {
if (isNew) {
this.urlControl.setValidators([trimmedRequired(), httpsUrlValidator()]);
} else {
this.urlControl.clearValidators();
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
apps/lfx-one/src/app/shared/components/accept-invite-organization-dialog/accept-invite-organization-dialog.component.ts:52
isNewOrgno longer reflects whether the org is “new” (it’s now true whenever an org name is present, even if the org was selected/resolved). Rename to match the updated semantics (e.g.,hasOrganizationName/requiresUrlValidation) so future changes don’t accidentally reintroduce incorrect validator toggling.
protected readonly isNewOrg = computed(() => {
const value = this.formValue();
return !!value?.organization?.trim();
});
audigregorie
left a comment
There was a problem hiding this comment.
Code Review Summary
Solid prefill implementation — the destroy/route-reuse guards and the duplicate-CDP-POST avoidance in accept() are correct and well-reasoned. One maintainability issue (a now-misleading computed name) and a few doc/timeout-placement nits below.
What's done well
- The
destroyedflag +cleanupDestroyListenerguard incommittee-view.component.tscorrectly handles thefirstValueFromdefaultValuemasking teardown — a subtle Angular quirk that would otherwise open the dialog over a new route. - The
committeeId() !== committee.uidpost-dialog guard correctly handles Angular's route-reuse (same component instance, different:id) — a case the destroy guard alone wouldn't catch. - Restructuring
accept()to skippreResolveOrganizationon the employer-fallback path avoids a duplicate CDP search + find-or-create POST, halving latency and eliminating a redundant side-effecting call.
jordane
left a comment
There was a problem hiding this comment.
Solid iteration — the bot round-trips landed real fixes, and the destroy/route-reuse guards are correct. Confirming for the record that Copilot's repeated DestroyRef.onDestroy claim is wrong: the abstract class types it as (callback: () => void) => () => void, so cleanupDestroyListener() is valid. Your rebuttal stands.
Three things I'd like addressed before merge, all in the prefill path (see inline comments):
- When the profile employer can't be domain-resolved, the dialog now opens with a name prefilled, an empty URL, and a red validation warning already showing — with no visible website field to fix it outside manual mode. Pre-PR the dialog just opened blank.
- The two sequential
timeout(2000)calls stack to a ~4s worst-case spinner; the comments say ≤2s. - The find-or-create CDP POST still runs on every Join/Apply click. Since
organization_idis stripped downstream anyway, prefilling straight from the search match's domain would drop the write and halve the latency.
Rest are nits. Two PR-shape items worth a mention: the branch name is feat/LFXV2-2690-org-prefill-join-flow (repo convention is feat/LFXV2-2690, no descriptive suffix), and commit bbde395c2 uses chore: merge main into ..., which commitlint rejects — a rebase onto main instead of the merge commit would clear both.
- Only prefill the org dialog when a domain was resolved — returning null from resolveCurrentEmployer when website is unset prevents the dialog from opening with orgInvalid=true and showOrgWarning immediately visible - Replace resolveOrganization find-or-create POST in resolveOrgDomain with a direct prefill from the search match (name + normalised domain); removes an unnecessary write call now invoked from open-join and apply flows - Rename isNewOrg → hasOrgName in the dialog component and template; the computed now means "org name is present" (not "org lacks CDP id") so the name matches the semantics and prevents a future reader from restoring the old !organization_id guard and reintroducing the validator-bypass bug - Add comment to toObservable block explaining why effect() is not used (effect() forbids signal writes without allowSignalWrites: true) - Update ≤2 s comments to reflect ≤4 s worst case (two sequential 2 s timeouts: work-experiences GET + CDP domain resolve) - Update resolvingOrg signal comment to reflect it stays true for the whole dialog session (not just the prefetch phase) via .finally() - Extract trimmed name const in resolveOrgDomain; remove non-null assertions - Add JSDoc to preResolveOrganization LFXV2-2690 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (1)
apps/lfx-one/src/app/modules/committees/committee-view/committee-view.component.ts:858
- The route-ID checks in
handleJoinRequestrun only after this promise resolves, which is after the dialog closes. Since Angular reuses this component across/groups/:id, navigating from A to B during this prefetch leavesdestroyedfalse and opens A's Confirm Organization dialog over B; B's CTA also remains loading until that stale dialog closes. Capture the starting committee ID and return beforedialogService.openwhen it changes (or cancel the lookup on ID changes).
const prefillOrg = await firstValueFrom(this.invitationAcceptFlow.resolveCurrentEmployer().pipe(takeUntilDestroyed(this.destroyRef)), {
defaultValue: null,
});
cleanupDestroyListener();
if (destroyed) {
return null;
}

Summary
resolveCurrentEmployer()method onInvitationAcceptFlowServicethat fetches the user's work experiences, picks the current employer, and resolves its domain — mirroringpreResolveOrganizationfor invite payloadscommittee-viewbefore opening the Confirm Organization dialog on both the open-join and apply-to-join paths so the dialog is pre-filled from the user's profileisNewOrgandorgInvalidinAcceptInviteOrganizationDialogComponent:organization_urlis now always required becauseorganization_id(CDP UUID) is stripped before the payload reaches committee-service, which requires name + domainBackground
When joining an open committee or applying to a committee that requires an organization (voting enabled or business email required), the Confirm Organization dialog was blank. The invite flow already pre-filled from the invite record, but open-join and apply paths had no equivalent source. This caused a confusing UX and, for apply flows, meant the submitted org was missing — leading to
organization id or organization name and domain are requirederrors when an admin approved the application.The back-end counterpart (storing org on the application record and using it at approve time) is in lfx-v2-committee-service#174.
Test plan
enable_voting: true— dialog pre-fills with profile employerResolves: LFXV2-2690
Made with Cursor