Skip to content

feat(invitations): complete external ID bulk invite flow and redesign result dialog#8406

Draft
LWS49 wants to merge 1 commit into
lws49/feat-add-ext-idfrom
lws49/feat-ext-id-invite-flow
Draft

feat(invitations): complete external ID bulk invite flow and redesign result dialog#8406
LWS49 wants to merge 1 commit into
lws49/feat-add-ext-idfrom
lws49/feat-ext-id-invite-flow

Conversation

@LWS49
Copy link
Copy Markdown
Collaborator

@LWS49 LWS49 commented May 26, 2026

Summary

Completes the external ID bulk invite flow started in the previous PR. The CSV upload now acts as a full upsert for external IDs on existing records: a nil ext_id is filled in, a non-nil ext_id that differs from the CSV value is overwritten (not rejected), and a blank CSV cell preserves the existing value. The external_id_conflict error condition is removed - the only remaining hard rejection for ext_id is external_id_taken (the CSV value is already held by a different course member). The invite result dialog is redesigned: six conditional flat sections replace the previous accordion layout, updated rows fold into the Existing Invitations and Existing Course Users sections with a bold ext_id and old-value tooltip, and the separate "External IDs Updated" section is removed.

Design decisions

  • external_id_conflict treated as upsert, not error - CSV uploads are submitted exclusively by course staff (trusted actors). The real safety guard is external_id_taken (ID collision across different people), which is still a hard error. Flagging a staff member correcting a previous ext_id entry as a conflict imposes workflow cost with no safety gain beyond what external_id_taken already provides. Nil→value and non-nil→different-value are now treated symmetrically.
  • Updated rows merged into existing sections - folding updated rows into Existing Invitations/Course Users with a bold ext_id badge and "Previously: X" tooltip is sufficient signal. A dedicated "External IDs Updated" section created a confusing fourth category; updated users are conceptually still existing users.
  • Flat conditional sections, no accordion - the accordion hid the primary result (who was invited) by default. Conditional rendering already handles clutter since empty sections don't appear. The common case (all-new batch) renders exactly two sections with no interaction required.
  • Needs Attention at top - duplicates require the admin to fix the CSV and re-upload. Placing them below success sections risks the admin closing the dialog without noticing. This matches master's original ordering intent.
  • Save order: updated records before new records - within the transaction, updated invitations and course users are saved before new ones. This is required for freed-ID batch claims: if Alice changes from freed-id to new-id and Bob claims freed-id in the same batch, Alice's update must persist first so Bob's insert doesn't hit the unique constraint.

Cross-type freed-ID ordering note

When an existing course user frees an ext_id and a brand-new user (no existing Coursemology account) claims it in the same batch, the new user is still rejected with external_id_taken. This is because invite_new_users runs before add_existing_users in the processing pipeline, so the new invitation is evaluated before the existing course user releases the ID. This is a known ordering limitation documented in the spec (test: document cross-type freed-id ordering behavior). The same-type freed-ID case (both are existing invitations, or both are existing course users) works correctly.

Regression prevention

Specs cover: nil→value upsert for invitations and course users; non-nil→different-free-value upsert with previous_external_id capture; external_id_taken still fires when the target ID is held by another member; freed-ID batch claim within the same record type; cross-type freed-ID ordering behavior (documented as a known limitation); previousExternalId rendered in Jbuilder response.

Frontend tests cover: InvitationResultSkippedTable updated-row rendering (bold + badge + tooltip); InvitationResultDialog updated rows appear in Existing Invitations section, no "External IDs Updated" heading; InvitationResultUsersTable reason text assertions for all three remaining duplicate reasons.

Manual testing: all 7 scenarios above verified - happy path new batch, nil→value upsert with badge, non-nil→different upsert with tooltip, external_id_taken Needs Attention, freed-ID same-type batch, existing user no-change, dialog layout with no External IDs Updated section.

Backward compatibility: existing invite flows unaffected. The only behavioral change for existing data is that re-inviting a user whose ext_id differs from the CSV value now updates the record instead of rejecting it.

@LWS49 LWS49 changed the base branch from master to lws49/feat-add-ext-id May 26, 2026 07:36
@LWS49 LWS49 changed the title feat(invitations): complete external ID bulk invite flow feat(invitations): complete external ID bulk invite flow and redesign result dialog May 26, 2026
@LWS49 LWS49 force-pushed the lws49/feat-ext-id-invite-flow branch 3 times, most recently from 0227e24 to c0dcc05 Compare May 26, 2026 08:09
… result dialog

Ext ID is now accepted in bulk invite CSVs. For existing enrolled users and pending invitations, if external ID is provided, it upserts the current external ID. Conflicts due to duplicate emails or external IDs route to Needs Attention, with clear reasons explaining what to be done.
@LWS49 LWS49 force-pushed the lws49/feat-ext-id-invite-flow branch from c0dcc05 to e892fd9 Compare May 26, 2026 15:12

context 'when an existing course user has non-nil ext_id and CSV provides a free different value' do
it 'updates the course_user external_id' do
result = subject.invite(enrolled_conflict_user_attrs)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lint/UselessAssignment: Useless assignment to variable - result.

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.

1 participant