Skip to content

fix: stable Member.Key for invited members + Member.Name promotion#62

Merged
poxet merged 2 commits into
masterfrom
feature/invited-member-edit-fix
May 10, 2026
Merged

fix: stable Member.Key for invited members + Member.Name promotion#62
poxet merged 2 commits into
masterfrom
feature/invited-member-edit-fix

Conversation

@poxet
Copy link
Copy Markdown
Contributor

@poxet poxet commented May 10, 2026

Summary

Three-part fix to the invited-member name flow.

1. Stable Member.Key for invited members

TeamServiceRepositoryBase.AddTeamMemberAsync previously left Member.Key null when the consumer's CreateTeamMember did. Multiple invited members all sharing Key=null caused two visible bugs:

  • The <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.
  • Server-side team.Members.Single(x => x.Key == userKey) in TeamRepository.SetMemberNameAsync threw on duplicate nulls, and the surrounding .Where(x => x.Key != userKey) stripped every null-keyed sibling on save.

AddTeamMemberAsync now assigns Guid.NewGuid().ToString() when the consumer left Member.Key empty, alongside the existing Invitation/State auto-defaults. SetInvitationResponseAsync already swapped the placeholder for userKey on accept; that path is unchanged.

2. Promote Member.Name -> User.Name on accept (only-if-empty)

The admin-entered invitation name was previously stripped on accept and the new user's User.Name was whatever the IdP claim provided (or empty). Now, when an invitation is accepted:

  • TeamServiceBase.SetInvitationResponseAsync captures Member.Name before the accept clears it (via a new virtual hook GetInvitedMemberNameAsync, overridden in TeamServiceRepositoryBase with typed access to the team document).
  • TeamRepository.SetInvitationResponseAsync now clears Member.Name unconditionally on accept.
  • The captured name is forwarded to a new IUserService.SeedUserNameAsync(userKey, name). The default impl in UserServiceRepositoryBase<TUserEntity> only writes if User.Name is 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 of team:manage. SaveMemberName branches:

  • Self-edit -> IUserService.SetUserNameAsync (always overwrites) plus SetMemberNameAsync(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.
  • Admin-edit on someone else -> existing per-team override path, unchanged.

The Reset button is hidden for self-edit (no override-vs-default distinction), and the edit-mode gate now defensively requires _editingMemberKey to be non-empty so any legacy null-keyed members can't get stuck.

Consumer impact

API surface additions (non-breaking for consumers extending UserServiceRepositoryBase / using IUserRepository<TUserEntity> via DI):

  • IUserRepository<TUserEntity>: GetByKeyAsync(string userKey), SetNameAsync(string userKey, string name)
  • IUserService: SeedUserNameAsync, SetUserNameAsync (with Task.CompletedTask virtual no-op defaults on UserServiceBase)
  • TeamServiceBase: GetInvitedMemberNameAsync (virtual default returns null)

External implementers of the IUserService / IUserRepository interfaces 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 in Tharga.Team.Service.Tests).
    • SetInvitationResponseSeedTests (4): accept-with-name calls SeedUserNameAsync, accept-with-empty / whitespace name does not, reject does not.
    • UserServiceBaseDefaultsTests (2): both new virtual methods are no-ops by default.
  • Manual on the sample app: invite creates non-null Member.Key; multiple invited rows do not co-edit; save returns to read-only; accept clears Member.Name; self-edit propagates to User.Name globally without disturbing other teams' overrides.

poxet added 2 commits May 10, 2026 18:01
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
Copy link
Copy Markdown

codecov Bot commented May 10, 2026

Codecov Report

❌ Patch coverage is 19.23077% with 21 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...arga.Team.Blazor/Features/Team/TeamComponent.razor 0.00% 18 Missing ⚠️
Tharga.Team/UserServiceBase.cs 50.00% 2 Missing ⚠️
Tharga.Team/TeamServiceBase.cs 75.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@poxet poxet merged commit ed747ce into master May 10, 2026
4 of 6 checks passed
@poxet poxet deleted the feature/invited-member-edit-fix branch May 10, 2026 17:47
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