Skip to content

feat(committees): add bulk email invite to create-group wizard - #1078

Open
manishdixitlfx wants to merge 7 commits into
mainfrom
feat/LFXV2-2606-wizard-bulk-invite
Open

feat(committees): add bulk email invite to create-group wizard#1078
manishdixitlfx wants to merge 7 commits into
mainfrom
feat/LFXV2-2606-wizard-bulk-invite

Conversation

@manishdixitlfx

@manishdixitlfx manishdixitlfx commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

What

Adds a bulk email invite action to Step 4 (members) of the create-group wizard: an "Invite by Email" entry point that opens AddMemberDialogComponent to invite one or more people by email. Invites are sent immediately as pending committee invites (POST /invites).

Fix included (review finding)

openInviteByEmailDialog() originally passed existingInvites: [] hard-coded, making the dialog's already-invited dedupe inert. It now:

  • Fetches the committee's current invites via getCommitteeInvites(committeeId) before opening and passes them as existingInvites, so the dialog dedupes against people already invited.
  • Falls back to a session-accumulated invite list (deduped by normalized email, refreshed after each successful dialog close) if the live fetch fails, so repeat opens still dedupe.

Known limitations / follow-ups

  1. Pending invites aren't shown in the Step 4 list. Invites become pending invites (not members), so post-send feedback is dialog-only. Surfacing pending invites inline is a UX follow-up.
  2. Invites send immediately. Canceling the wizard does not un-send them.
  3. Fallback is best-effort. The dialog returns only a boolean (not the invited emails), so if both the post-close refresh and the next open's fetch fail, invites made in the just-closed dialog won't be in the session fallback. Removing this gap would require the dialog to return the invited emails.

Validation

yarn format clean, yarn lint 0 errors, yarn build passes. (apps/lfx-one has no unit-test runner, so no unit test added — consistent with the repo.)

🤖 Generated with Claude Code


Note

Medium Risk
Changes when committee invites are sent and batches them with other member operations on wizard completion; behavior is still the existing invite API with client-side staging and dedupe.

Overview
Adds bulk invite-by-email to the create-group members step with deferred sending: invites are staged client-side and only posted when the wizard finishes, so canceling the wizard sends nothing (LFXV2-2606).

AddMemberDialogComponent gains a collectOnly mode that validates emails and returns CreateCommitteeInviteRequest[] instead of calling POST /invites immediately; the submit action becomes “Add to invitations” in that mode. The members step shows a Pending invitations list (with remove), dedupes against members, pending server invites (edit), and already-staged emails, and wires staged invites through MemberPendingChanges.toInvite. committee-manage treats toInvite like other pending member ops and flushes them on Done via createCommitteeInvite.

Minor UX: member search/filters render only when there is at least one member; the manual add action is labeled “Add with details.”

Reviewed by Cursor Bugbot for commit 026080d. Bugbot is set up for automated code reviews on this repo. Configure here.

Wire AddMemberDialogComponent into the create-group wizard's Add Members
step as a primary 'Invite by email' CTA, so users can paste a
comma/semicolon/newline-separated email list instead of adding members
one at a time. The existing MemberFormComponent path is retained as the
secondary 'Add with details' action.

Also fix the members filter box on Step 4: hide it when the group has no
members yet (nothing to filter) and change its placeholder from
'Search members...' to 'Filter members…' so it can't be misread as an
invite input on an empty group.

LFXV2-2606

Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
openInviteByEmailDialog passed existingInvites: [], making the dialog's
already-invited dedupe inert. Fetch the committee's current invites before
opening and pass them as existingInvites. If the fetch fails, fall back to a
session-accumulated list of invites (deduped by email, refreshed on each
successful dialog close) so repeat opens still dedupe.

LFXV2-2606

Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
Copilot AI review requested due to automatic review settings July 8, 2026 17:50
@coderabbitai

coderabbitai Bot commented Jul 8, 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b4990ad9-e534-48cb-ba98-3ac974e8650f

📥 Commits

Reviewing files that changed from the base of the PR and between 026080d and 62b4568.

📒 Files selected for processing (1)
  • apps/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.ts

Walkthrough

Committee member editing now stages invite-by-email entries locally, displays pending invitations, and carries staged invite requests into the committee wizard for submission as invite operations.

Changes

Invite by Email Flow

Layer / File(s) Summary
Invite dialog collect-only mode
packages/shared/src/interfaces/member.interface.ts, apps/lfx-one/src/app/modules/committees/components/add-member-dialog/add-member-dialog.component.ts, apps/lfx-one/src/app/modules/committees/components/add-member-dialog/add-member-dialog.component.html
MemberPendingChanges adds toInvite, and the add-member dialog supports either immediate invite creation or collect-only submission with staged invite requests and corresponding button text/icon.
Pending invite staging in members manager
apps/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.ts, apps/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.html
The members manager loads pending server invites, stages and deduplicates local invites, renders pending invitations with removal controls, and emits toInvite through member updates.
Wizard invite operations
apps/lfx-one/src/app/modules/committees/committee-manage/committee-manage.component.ts
The committee wizard treats staged invites as pending member updates and appends committee invite operations during final submission.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding bulk email invites to the create-group wizard.
Description check ✅ Passed The description directly matches the changeset, describing bulk invite-by-email, staging, deduping, and deferred sending.
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 feat/LFXV2-2606-wizard-bulk-invite

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Comment @coderabbitai help to get the list of available commands.

Copilot AI 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.

Pull request overview

This PR wires a bulk email invite action into Step 4 (members) of the create-group wizard. It adds an "Invite by email" button that opens the existing AddMemberDialogComponent to invite one or more people by email; invites are sent immediately as pending committee invites (POST /invites). To make the dialog's already-invited dedupe effective, the component fetches the committee's current invites before opening and accumulates them in a session list as a fallback when the live fetch fails. The template is also cleaned up: the primary button is relabeled and the search/filter row is hidden until the group has members.

Changes:

  • Add openInviteByEmailDialog() / openInviteDialog() that open AddMemberDialogComponent, passing current members and invites for dedupe.
  • Add a session-accumulated invite list (sessionInvites + rememberInvites() / refreshSessionInvites()) as a fallback source when the live invites fetch fails.
  • Template: add the "Invite by email" button, rename "Add Member" → "Add with details", and gate the search/filter row behind @if (memberCount() > 0).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
committee-members-manager.component.ts Adds invite-by-email dialog wiring plus a session-accumulated, email-deduped invite fallback list feeding the dialog's already-invited dedupe.
committee-members-manager.component.html Adds the "Invite by email" button, relabels the details button, and hides the search/filter row until members exist.

Note: The accumulated invites are not filtered by status before being used for dedupe. Since getCommitteeInvites() returns invites of every status and the repo convention (committee-view.component.ts:777-781) is to keep only pending, declined/revoked invitees would be incorrectly blocked from re-invitation. See the inline comment for a self-contained fix.


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

@manishdixitlfx
manishdixitlfx marked this pull request as ready for review July 8, 2026 18:17
@manishdixitlfx
manishdixitlfx requested a review from a team as a code owner July 8, 2026 18:17

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

🧹 Nitpick comments (2)
apps/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.ts (2)

139-163: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No guard against concurrent invocations.

Rapid clicks on the "Invite by email" button trigger overlapping getCommitteeInvites calls with no in-flight guard, each of which independently resolves and calls openInviteDialog(), potentially stacking multiple dialogs.

🤖 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/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.ts`
around lines 139 - 163, The openInviteByEmailDialog flow does not prevent
multiple concurrent executions, so repeated clicks can trigger overlapping
getCommitteeInvites requests and openInviteDialog calls. Add an in-flight guard
in CommitteeMembersManagerComponent around openInviteByEmailDialog (for example,
a boolean flag or similar state) so a second invocation is ignored until the
current invite load/subscription completes or errors, and make sure the guard is
cleared in the subscription/error path before allowing another dialog open.

269-269: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Passing internal array by reference to dialog data.

existingInvites: this.sessionInvites shares the live internal array reference with the opened dialog rather than a defensive copy. If AddMemberDialogComponent ever mutates this input, it would silently corrupt the parent's session state.

🛡️ Defensive copy
-        existingInvites: this.sessionInvites,
+        existingInvites: [...this.sessionInvites],
🤖 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/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.ts`
at line 269, The dialog data is passing the live sessionInvites array by
reference, which can let AddMemberDialogComponent mutate parent state
unexpectedly. Update the data passed from committeeMembersManagerComponent’s
dialog-opening logic so existingInvites uses a defensive copy instead of
this.sessionInvites, keeping the parent’s sessionInvites isolated from any
dialog-side changes.
🤖 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/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.ts`:
- Around line 139-163: The openInviteByEmailDialog flow does not prevent
multiple concurrent executions, so repeated clicks can trigger overlapping
getCommitteeInvites requests and openInviteDialog calls. Add an in-flight guard
in CommitteeMembersManagerComponent around openInviteByEmailDialog (for example,
a boolean flag or similar state) so a second invocation is ignored until the
current invite load/subscription completes or errors, and make sure the guard is
cleared in the subscription/error path before allowing another dialog open.
- Line 269: The dialog data is passing the live sessionInvites array by
reference, which can let AddMemberDialogComponent mutate parent state
unexpectedly. Update the data passed from committeeMembersManagerComponent’s
dialog-opening logic so existingInvites uses a defensive copy instead of
this.sessionInvites, keeping the parent’s sessionInvites isolated from any
dialog-side changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e0aa2a14-a8b6-4da4-a94c-391a9975e949

📥 Commits

Reviewing files that changed from the base of the PR and between 1bcbb98 and 9d497dc.

📒 Files selected for processing (2)
  • apps/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.html
  • apps/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.ts

@jordane jordane left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Invites send immediately. Canceling the wizard does not un-send them.

This seems like a non-starter for merging this. Why would a wizard form, which hasn't been submitted be taking action immediately on that page with no clear indication to the user? That's a recipe for a bad UX. I suggest considering a few alternatives:

  1. Fix it to send the bulk invites after the wizard is fully submitted, and not immediately action it
  2. OR move the bulk invite outside of the creation (it's useful after group creation too!) and then update the creation wizard to inform the user that they can initiate a bulk invite after the group is created.

Invites triggered from the create-group wizard's Add Members step were
sent immediately (POST /invites) the moment the dialog was submitted, so
cancelling or abandoning the wizard still fired real invitations with no
clear indication to the user (PR #1078 review).

Rework the bulk-invite flow to stage invites client-side and flush them
only when the wizard is completed:

- AddMemberDialogComponent gains a collectOnly mode that validates and
  returns the built invite payloads instead of POSTing them; the group
  management page keeps immediate-send behavior (default).
- The members manager stages returned invites in a pendingInvites signal
  (deduped by email), surfaces them in a 'Pending invitations' section
  with per-row remove, and emits them via MemberPendingChanges.toInvite.
- The wizard flushes toInvite through createCommitteeInvite in its Done
  handler, alongside member add/update/delete ops. Skip/Cancel sends
  nothing.

LFXV2-2606

Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
Copilot AI review requested due to automatic review settings July 8, 2026 21:03
@manishdixitlfx

Copy link
Copy Markdown
Contributor Author

Thanks @jordane — you're right, immediate send from an unsubmitted wizard was the wrong behavior. Fixed in d04a122 by going with your option (1): invites are now staged, not sent, until the wizard is completed.

What changed:

  • AddMemberDialogComponent gains a collectOnly mode — it validates and returns the built invite payloads instead of calling POST /invites. The group management page keeps immediate-send (default), since sending now is correct there.
  • Step 4 stages the returned invites in a pendingInvites signal (deduped by email) and shows them in a "Pending invitations" section with a per-row remove, so there's a clear indication of what will be sent.
  • The wizard's Done handler flushes the staged invites via createCommitteeInvite, alongside the existing member add/update/delete ops. Skip For Now / Cancel / closing the wizard sends nothing.

So the flow now matches the rest of Step 4 (members added via "Add with details" were already deferred to Done) — nothing fires until the explicit finish action.

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

🧹 Nitpick comments (1)
apps/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.html (1)

45-54: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Prefer (onClick) on lfx-button here. It matches the component’s public output and keeps this handler aligned with the button API used in other places.

🤖 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/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.html`
around lines 45 - 54, The pending-invite remove action is wired to the native
click event instead of the lfx-button component API. Update the handler in
committee-members-manager.component.html to use the button’s public onClick
output for the lfx-button used in the removePendingInvite flow, matching the
event pattern already used elsewhere and keeping the invite.invitee_email
removal logic intact.

Source: Learnings

🤖 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/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.html`:
- Around line 45-54: The pending-invite remove action is wired to the native
click event instead of the lfx-button component API. Update the handler in
committee-members-manager.component.html to use the button’s public onClick
output for the lfx-button used in the removePendingInvite flow, matching the
event pattern already used elsewhere and keeping the invite.invitee_email
removal logic intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f71ee6ef-6454-4c46-8b0a-0592b49a2681

📥 Commits

Reviewing files that changed from the base of the PR and between 9d497dc and d04a122.

📒 Files selected for processing (6)
  • apps/lfx-one/src/app/modules/committees/committee-manage/committee-manage.component.ts
  • apps/lfx-one/src/app/modules/committees/components/add-member-dialog/add-member-dialog.component.html
  • apps/lfx-one/src/app/modules/committees/components/add-member-dialog/add-member-dialog.component.ts
  • apps/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.html
  • apps/lfx-one/src/app/modules/committees/components/committee-members-manager/committee-members-manager.component.ts
  • packages/shared/src/interfaces/member.interface.ts

Copilot AI 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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings July 8, 2026 21:19

Copilot AI 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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Address review comments from copilot-pull-request-reviewer, cursor:

- committee-members-manager.component.ts: filter fetched committee invites
  to pending status before feeding the invite-by-email dedupe, so declined
  or revoked invitees can still be re-invited (matches the convention in
  committee-view.component.ts:781) (per copilot-pull-request-reviewer)
- add-member-dialog.component.ts: set the submitting guard at the top of
  onSubmit again, before the async organization resolution, to prevent a
  double-click during that window from firing duplicate invite calls
  (per cursor)

Resolves 4 review threads.

Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
Copilot AI review requested due to automatic review settings July 9, 2026 15:06
@manishdixitlfx

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed

Commit: 026080d

Changes Made

  • committee-members-manager.component.ts: filter fetched committee invites to pending status before feeding the invite-by-email dedupe, so declined/revoked invitees can still be re-invited — matches the convention in committee-view.component.ts:781 (per copilot-pull-request-reviewer)
  • add-member-dialog.component.ts: restored the submitting guard to the top of onSubmit(), before the async organization resolution, preventing a double-click during that window from firing duplicate invite calls (per cursor)

Threads Resolved

4 of 4 unresolved threads addressed (3 were the same pending-status finding from copilot-pull-request-reviewer; 1 double-submit finding from cursor).

Note

The earlier design change requested by @jordane (invites must not send immediately from the wizard) was addressed in d04a122 — invites are now staged and only sent on wizard completion. Re-requested his review for confirmation.

this.pendingInvites.update((current) => [...current, ...additions]);
this.emitMemberUpdates();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Staged invite survives member add

Medium Severity

Staging an email invite then adding the same address via Add with details leaves that address in pendingInvites and toInvite. Finishing the wizard runs both member creation and createCommitteeInvite for one person.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 026080d. Configure here.

this.form.patchValue({ organization_id: result.id || null, organization: result.name });
}
fanOut(buildCommitteeOrganizationPayload(this.organizationFormValue()));
complete(buildCommitteeOrganizationPayload(this.organizationFormValue()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cancel blocked during org resolve

Medium Severity

onSubmit sets submitting before async organization resolution, and Cancel is disabled while submitting is true. During a slow or hung resolveCurrentEntry call, the invite dialog cannot be dismissed except by waiting for resolution to finish.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 026080d. Configure here.

Copilot AI 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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment on lines +558 to +563
// Send staged bulk email invites — deferred here so they only fire on wizard completion (LFXV2-2606)
if (memberUpdates.toInvite.length > 0) {
for (const invite of memberUpdates.toInvite) {
operations.push(this.createMemberOperation('invite', () => this.committeeService.createCommitteeInvite(committeeId, invite)));
}
}

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

PR #1078feat(committees): add bulk email invite to create-group wizard

⚠️ PR description is materially inaccurate — please correct before merge. The hand-written summary says "Invites are sent immediately as pending committee invites (POST /invites)" and the "Fix included" block describes a fetch-based dedupe approach that wraps an immediate-send flow. The actual implementation is the opposite: collectOnly mode stages payloads in pendingInvites, they are flushed via createCommitteeInvite only when the wizard completes (matching the Cursor BugBot summary, not the hand-written one). Merging with an inaccurate description creates a misleading git trail and could cause future reviewers to revert the deferred-send architecture.

Overview (actual behavior): Adds "Invite by Email" to Step 4 of the create-group wizard. AddMemberDialogComponent gains a collectOnly flag that, when true, builds CreateCommitteeInviteRequest[] payloads and returns them via dialogRef.close(staged) instead of posting immediately. The wizard stages them in pendingInvites, surfaces a "Pending invitations" list with per-invite remove, dedupes against both server-side pending invites and already-staged ones, and flushes via createCommitteeInvite on Done. Canceling the wizard sends nothing (LFXV2-2606 requirement met ✅).

Secrets / sensitive data: None found.

Angular 20 audit:

  • ✅ No signal-reading method calls in templates — pendingInvites() is a signal read; committeeLabel.toLowerCase() is a built-in string method on a property, not a component method call; (click)="openInviteByEmailDialog()" and (click)="removePendingInvite(..." are event bindings ✅
  • collectOnly in [label]="collectOnly ? ... : ..." is a plain boolean property access — not a method call ✅
  • @for (invite of pendingInvites(); track invite.invitee_email) — signal read in @for, property access in track
  • ✅ No effect() introduced
  • destroyRef = inject(DestroyRef) (line 60, pre-existing) used correctly with takeUntilDestroyed
  • take(1) on existing$ and dialogRef.onClose.pipe(take(1))

Interface / constants placement:

  • MemberPendingChanges.toInvite added to packages/shared/src/interfaces/member.interface.ts
  • CreateCommitteeInviteRequest cross-imported from committee.interface within the same shared package ✅

Logic correctness:

  • stageInvites dedupes by normalized email before appending — correctly prevents double-staging
  • openCollectInviteDialog passes [...serverInvites, ...stagedAsInvites] with the staged list as Pick<CommitteeInvite, 'invitee_email'>[] — the comment correctly notes the dialog only reads invitee_email for deduplication ✅
  • ✅ Status filter: invite.status.toLowerCase() === 'pending' — only pending server invites block re-invite; accepted/declined/revoked are correctly passable ✅
  • committee-manage.ts: toInvite flushed last in the operations loop, after add/update/delete — correct ordering ✅

Process:

  • 🟡 Validation section omits yarn check-types; no ## Test plan heading
  • 🟡 PR description inaccuracy (detailed above)

Verdict: ❌ BLOCKED — 1 race condition, 2 process warnings (see inline comment)

)
: of([] as CommitteeInvite[]);

existing$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((serverInvites) => {

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.

🟡 Warning — race condition: multiple rapid clicks open multiple dialogs

openInviteByEmailDialog() fires a network request (getCommitteeInvites) and opens the dialog inside the subscription callback. If the user clicks "Invite by Email" again before the first fetch resolves (~100–300ms), a second subscription starts and a second dialog opens when that fetch completes — both dialogs are live simultaneously.

Suggested fix — guard with a loading flag:

// In the class:
private inviteDialogLoading = false;

public openInviteByEmailDialog(): void {
  if (this.inviteDialogLoading) return;   // ← guard
  this.inviteDialogLoading = true;

  const committeeId = this.committeeId();
  const existing$ = committeeId
    ? this.committeeService.getCommitteeInvites(committeeId).pipe(
        take(1),
        catchError((error) => {
          console.error('Failed to load existing invites for dedupe:', error);
          return of([] as CommitteeInvite[]);
        })
      )
    : of([] as CommitteeInvite[]);

  existing$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((serverInvites) => {
    this.inviteDialogLoading = false;      // ← reset before opening
    const pending = serverInvites.filter((i) => (i.status ?? '').toLowerCase() === 'pending');
    this.openCollectInviteDialog(pending);
  });
}

Alternatively, use a switchMap so a new click cancels the in-flight fetch before opening a fresh dialog.

openInviteByEmailDialog fed getCommitteeInvites() (all statuses) into the
dialog's already-invited dedupe unfiltered, so a declined or revoked invitee
was treated as already-invited and could not be re-invited. Filter to pending
at the fetch sink, matching committee-view.component.ts.

Addresses PR #1078 review feedback (copilot-pull-request-reviewer).

LFXV2-2606

Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
Copilot AI review requested due to automatic review settings July 15, 2026 16:50
@cursor

cursor Bot commented Jul 15, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches committee invite batching on wizard completion and shared MemberPendingChanges; wrong timing or dedupe could send duplicate invites or skip sends, but scope is wizard UX rather than auth.

Overview
Bulk email invites in the create-group wizard are staged client-side and only sent when the user finishes the wizard (LFXV2-2606), so canceling does not trigger POST /invites.

AddMemberDialogComponent gains collectOnly mode: it validates emails and returns CreateCommitteeInviteRequest[] instead of calling the API. The members step adds Invite by email (collect mode), a Pending invitations list with remove, and dedupes against existing members, pending server invites (edit path), and already-staged emails. MemberPendingChanges includes toInvite, and committee-manage flushes those invites in buildMemberOperations on Done / edit save alongside member CRUD.

Minor UX on step 4: Add with details vs invite-by-email, member search/filters shown only when there is at least one member, and submit is locked during async org resolution to prevent double-send in immediate mode.

Reviewed by Cursor Bugbot for commit 62b4568. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ 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 62b4568. Configure here.

// Accepted invitees are already members; declined/revoked ones must be re-invitable.
const pending = serverInvites.filter((invite) => (invite.status ?? '').toLowerCase() === 'pending');
this.openCollectInviteDialog(pending);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rapid clicks open multiple dialogs

Medium Severity

openInviteByEmailDialog() starts getCommitteeInvites and only opens AddMemberDialogComponent inside the subscription. There is no in-flight guard or disabled state on the button, so repeated clicks before the first request finishes start parallel fetches and each completion opens another modal dialog.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 62b4568. Configure here.

@manishdixitlfx

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed

Commit: 62b4568

Changes Made

  • committee-members-manager.component.ts: filtered the fetched invites to pending status before they feed the invite dialog's already-invited dedupe (openInviteByEmailDialog), matching the convention in committee-view.component.ts. Previously all statuses were passed through, so a declined or revoked invitee was treated as already-invited and could not be re-invited. (per copilot-pull-request-reviewer)

Threads Resolved

Both Copilot pending-status threads resolved.

Separately Addressed

  • The "invites send immediately in an unsubmitted wizard" concern (@jordane) was reworked in d04a122 — invites are now staged client-side (collectOnly dialog) and flushed only on wizard completion; Skip/Cancel sends nothing. Reply + re-review handled by the author.

Not Changed (open nits)

  • Concurrent-open guard on the invite button and (click)(onClick) on lfx-button — trivial CodeRabbit nits, left for author discretion.

Copilot AI 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.

Pull request overview

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

// Bulk email invites staged in the wizard, deduped by normalized email. These are collected
// client-side and flushed by the wizard on completion (POST /invites) — never sent immediately,
// so cancelling the wizard sends nothing (LFXV2-2606). Surfaced as a "Pending invitations" list.
public readonly pendingInvites = signal<CreateCommitteeInviteRequest[]>([]);

<div class="flex items-center mb-4">
<div class="flex items-center gap-3">
<lfx-button size="small" label="Invite by email" icon="fa-light fa-envelope" data-testid="invite-by-email-button" (click)="openInviteByEmailDialog()">
changes: this.stripMetadata(m), // Pass entire member object, not just changed fields
})),
toDelete: members.filter((m) => m.state === 'deleted').map((m) => m.uid),
toInvite: this.pendingInvites(),
Comment on lines +559 to +562
if (memberUpdates.toInvite.length > 0) {
for (const invite of memberUpdates.toInvite) {
operations.push(this.createMemberOperation('invite', () => this.committeeService.createCommitteeInvite(committeeId, invite)));
}
Comment on lines +559 to +562
if (memberUpdates.toInvite.length > 0) {
for (const invite of memberUpdates.toInvite) {
operations.push(this.createMemberOperation('invite', () => this.committeeService.createCommitteeInvite(committeeId, invite)));
}
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.

4 participants