fix: stable Member.Key for invited members + Member.Name promotion#62
Merged
Conversation
Three-part fix to the invited-member name flow: 1. **Stable Member.Key for invited members.** Previously TeamServiceRepositoryBase.AddTeamMemberAsync left Member.Key null when the consumer's CreateTeamMember did. Multiple invited members all shared Key=null, causing the inline-edit gate (_editingMemberKey == context.Key) to match every null-keyed row indefinitely after a save, and the server-side .Single in TeamRepository.SetMemberNameAsync to throw. AddTeamMemberAsync now assigns Guid.NewGuid().ToString() when the member arrives without a Key, alongside the existing Invitation/State auto-defaults. SetInvitationResponseAsync swaps the placeholder to userKey on accept (already in place). 2. **Promote Member.Name to User.Name on accept (only-if-empty).** New IUserService.SeedUserNameAsync (virtual no-op default; real impl on UserServiceRepositoryBase) updates User.Name only when it is currently empty, so an IdP-provided name always wins. TeamServiceBase pre-captures the invited member's name via a new virtual hook GetInvitedMemberNameAsync (overridden in TeamServiceRepositoryBase) before the accept call clears Member.Name, then forwards to SeedUserNameAsync. TeamRepository.SetInvitationResponseAsync now clears Member.Name unconditionally on accept. 3. **User can edit their own member row -> updates User.Name.** TeamComponent.razor: pencil/edit gate now allows _user.Key == context.Key regardless of team:manage. SaveMemberName branches: self-edit calls IUserService.SetUserNameAsync (always overwrites) plus SetMemberNameAsync(null) to clear the per-team override; admin-edit on other rows is unchanged. Reset button hidden for self-edit. Defensive null-guard on the edit-mode equality so any legacy null-keyed members no longer stick the editor. API surface additions (non-breaking for consumers using UserServiceRepositoryBase / IUserRepository<TUserEntity> via DI): - IUserRepository: GetByKeyAsync, SetNameAsync - IUserService: SeedUserNameAsync, SetUserNameAsync (virtual no-op defaults on UserServiceBase) - TeamServiceBase: GetInvitedMemberNameAsync (virtual default returns null) 6 new tests in Tharga.Team.Service.Tests cover the orchestration (SetInvitationResponseSeedTests x4, UserServiceBaseDefaultsTests x2). 274 / 274 tests pass; build clean.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three-part fix to the invited-member name flow.
1. Stable
Member.Keyfor invited membersTeamServiceRepositoryBase.AddTeamMemberAsyncpreviously leftMember.Keynull when the consumer'sCreateTeamMemberdid. Multiple invited members all sharingKey=nullcaused two visible bugs:<TeamComponent />inline-edit gate (_editingMemberKey == context.Key) matched every null-keyed row indefinitely after a save, so editing a name on an invited row left the textbox stuck open and put every other invited row into edit mode at the same time.team.Members.Single(x => x.Key == userKey)inTeamRepository.SetMemberNameAsyncthrew on duplicate nulls, and the surrounding.Where(x => x.Key != userKey)stripped every null-keyed sibling on save.AddTeamMemberAsyncnow assignsGuid.NewGuid().ToString()when the consumer leftMember.Keyempty, alongside the existingInvitation/Stateauto-defaults.SetInvitationResponseAsyncalready swapped the placeholder foruserKeyon accept; that path is unchanged.2. Promote
Member.Name->User.Nameon accept (only-if-empty)The admin-entered invitation name was previously stripped on accept and the new user's
User.Namewas whatever the IdP claim provided (or empty). Now, when an invitation is accepted:TeamServiceBase.SetInvitationResponseAsynccapturesMember.Namebefore the accept clears it (via a new virtual hookGetInvitedMemberNameAsync, overridden inTeamServiceRepositoryBasewith typed access to the team document).TeamRepository.SetInvitationResponseAsyncnow clearsMember.Nameunconditionally on accept.IUserService.SeedUserNameAsync(userKey, name). The default impl inUserServiceRepositoryBase<TUserEntity>only writes ifUser.Nameis currently null/empty, so an IdP-provided name always wins.3. Users can edit their own member row
<TeamComponent />: the inline-edit pencil now appears on the current user's own row regardless ofteam:manage.SaveMemberNamebranches:IUserService.SetUserNameAsync(always overwrites) plusSetMemberNameAsync(team.Key, _user.Key, null)to clear the per-team override in the current team. Other teams' admin-set overrides for that user are untouched.The Reset button is hidden for self-edit (no override-vs-default distinction), and the edit-mode gate now defensively requires
_editingMemberKeyto be non-empty so any legacy null-keyed members can't get stuck.Consumer impact
API surface additions (non-breaking for consumers extending
UserServiceRepositoryBase/ usingIUserRepository<TUserEntity>via DI):IUserRepository<TUserEntity>:GetByKeyAsync(string userKey),SetNameAsync(string userKey, string name)IUserService:SeedUserNameAsync,SetUserNameAsync(withTask.CompletedTaskvirtual no-op defaults onUserServiceBase)TeamServiceBase:GetInvitedMemberNameAsync(virtual default returns null)External implementers of the
IUserService/IUserRepositoryinterfaces directly would see breakage; none known in this codebase. Suggest a minor version bump on next release.Existing teams with null-keyed invited members continue to work: the new defensive UI gate prevents the stuck-editor symptom, and the entries naturally heal as invitations flow through accept/reject.
Test plan
dotnet build -c Release-- 0 warnings, 0 errors.dotnet test -c Release-- 274 / 274 tests passing (was 268, +6 new orchestration tests inTharga.Team.Service.Tests).SetInvitationResponseSeedTests(4): accept-with-name callsSeedUserNameAsync, accept-with-empty / whitespace name does not, reject does not.UserServiceBaseDefaultsTests(2): both new virtual methods are no-ops by default.Member.Key; multiple invited rows do not co-edit; save returns to read-only; accept clearsMember.Name; self-edit propagates toUser.Nameglobally without disturbing other teams' overrides.