Skip to content

feat: TeamComponent inline edit of Member.Name (opt-in)#55

Merged
poxet merged 6 commits into
masterfrom
feature/team-member-name-edit
Apr 29, 2026
Merged

feat: TeamComponent inline edit of Member.Name (opt-in)#55
poxet merged 6 commits into
masterfrom
feature/team-member-name-edit

Conversation

@poxet
Copy link
Copy Markdown
Contributor

@poxet poxet commented Apr 29, 2026

Summary

Adds an opt-in inline edit affordance for Member.Name in <TeamComponent>'s member grid. Lets PlutusWave (feature 20) drop a workaround MemberNamesPanel. Off by default — existing consumers see no UI change.

Toggle

<TeamComponent TMember="..." EnableMemberNameEdit="true" />

When enabled and the caller has the team:manage scope, each row's Name column gets a pencil icon → textbox + Save / Reset / Cancel.

UX details

  • Edit prefill — textbox seeds with the resolved default (User.Name ?? ResolveDisplayName(user)) when no override is set, so the reviewer can see what they're overriding.
  • Auto-fallback — if the saved value matches the resolved default, persists null (no override). Avoids creating redundant overrides that just duplicate User.Name.
  • Reset button — explicit affordance to clear the override; only visible when one is currently set.
  • Override indicator — italicized name + tooltip showing the original User.Name (replaces a noisier badge approach).
  • Read-only fallback chain — column resolves Member.Name ?? User.Name ?? ResolveDisplayName(user), so clearing the override never leaves the cell empty.
  • Layout — buttons always visible: textbox uses flex: 1 1 0; min-width: 0; (shrinks freely), buttons flex: 0 0 auto; (hold size), stack flex-wrap: nowrap;.

Persistence

Shape A from the request: Platform persists, optional callback observes.

<TeamComponent TMember="..."
               EnableMemberNameEdit="true"
               OnMemberNameChanged="HandleMemberNameChanged" />

Consumer doesn't have to wire anything else for the change to land — Platform handles repository write + cache eviction + TeamsListChangedEvent + grid reload itself. The callback is purely observational.

Stack changes

Layer Change
Tharga.Team ITeamService.SetMemberNameAsync; ITeamManagementService.SetMemberNameAsync gated by [RequireScope(TeamScopes.Manage)]; TeamServiceBase evicts member cache + raises TeamsListChangedEvent; new public record MemberNameChangedArgs(TeamKey, MemberKey, OldName, NewName)
Tharga.Team.MongoDB ITeamRepository.SetMemberNameAsync + TeamRepository<...> impl (trims, whitespace → null); TeamServiceRepositoryBase override
Tharga.Team.Service AuditingTeamServiceDecorator covers it (action set-member-name)
Tharga.Team.Blazor EnableMemberNameEdit + OnMemberNameChanged parameters, conditional Name-column template with edit affordances

Bonus fixes (caught during sample testing)

  • UsersListView duplicate-key crash: added TeamKey to UserTeamInfo (also a useful API improvement) and defensive GroupBy(TeamKey, MemberKey) in the lookup. Fixes a Radzen DataGrid sibling-key collision when the user-team set had any duplicate row.
  • Sample wiring for /users: new AppUserAdminService (sample-only) backed by IUserRepositoryCollection<UserEntity>. UsersPage switched from bare <UsersView> to <UsersListView ActionsTemplate=...> + <TeamsListView>, with edit (dialog) and delete (confirm) buttons calling the admin service. Demonstrates the consumer-owned admin pattern from the original UsersView request.

Tests

7 new tests (3 service-level, 1 audit, 3 component-shape). 255 total passing.

Test plan

  • Build + security checks pass
  • PlutusWave's feature 20 can drop MemberNamesPanel and use <TeamComponent EnableMemberNameEdit="true" />

poxet added 6 commits April 29, 2026 13:50
Adds an opt-in inline edit affordance for Member.Name in
TeamComponent's member grid. PlutusWave (feature 20) needs this to
drop a workaround MemberNamesPanel; consumers that haven't opted in
see no UI change.

Toggle:
  <TeamComponent TMember="..." EnableMemberNameEdit="true" />

When enabled and the caller has the team:manage scope, each row's
Name column gets a pencil icon -> textbox + Save / Reset / Cancel.
Empty/whitespace input clears the override (writes null). An
"override" badge renders next to names that differ from the global
User.Name.

Persistence shape A: Platform persists, optional callback observes:
  [Parameter] public EventCallback<MemberNameChangedArgs>
      OnMemberNameChanged { get; set; }

Stack changes:
- ITeamService.SetMemberNameAsync (passes through to a new
  protected SetTeamMemberNameAsync); public method evicts member
  cache and raises TeamsListChangedEvent
- ITeamManagementService.SetMemberNameAsync gated by
  [RequireScope(TeamScopes.Manage)]
- ITeamRepository<...>.SetMemberNameAsync; implementation in
  TeamRepository<...> trims input, treats whitespace as null
- AuditingTeamServiceDecorator coverage with action `set-member-name`
- New public record MemberNameChangedArgs in Tharga.Team

7 new tests; 255 total passing.
…sTemplate in sample

TeamComponent fixes (from manual testing on the sample):
- Read-only Name column now resolves the fallback chain
  Member.Name ?? User.Name ?? ResolveDisplayName(user) so clearing
  the override displays the global User.Name (was rendering empty)
- Wrap name + edit button in a horizontal RadzenStack and use an
  inline <span> instead of block-level RadzenText so they no longer
  wrap to separate lines
- Replace the "override" badge with an italic name + tooltip
  showing the original User.Name. Cleaner, less visual noise.

Sample wiring for /users:
- New AppUserAdminService (sample-only) backed by
  IUserRepositoryCollection<UserEntity>; exposes UpdateAsync /
  DeleteAsync. Demonstrates the consumer-owned admin service pattern
  the original UsersView request expected.
- UsersPage now renders <UsersListView ActionsTemplate=...> +
  <TeamsListView> (replaces the bare <UsersView> wrapper) so it can
  inject edit / delete buttons.
- Edit opens a Radzen dialog (Name + Email), Delete confirms then
  deletes; both call NavigationManager.Refresh() to reload.
- Page restricted to Roles=Developer to match the original wrapper.

255 tests still passing.
UserTeamInfo is a record (value equality). Radzen DataGrid uses the
row's data as its identity key, so two UserTeamInfo records with
identical (TeamName, AccessLevel, State) collide as sibling
RadzenDataGridRow keys, throwing
"More than one sibling has the same key value" during render.

Two changes:
- Add `TeamKey` field to UserTeamInfo. Each team is now distinguishable
  even when two teams share a display name. Consumers also benefit —
  they can look up the team without a name-match.
- Defensively GroupBy (TeamKey, MemberKey) in the lookup so any
  upstream data dup of the same (team, user) pair collapses to one.

The same UsersListView fix applies to consumers using ActionsTemplate
that select a row and inspect Teams.
…k to null

- StartMemberNameEdit now seeds the textbox with the resolved default
  (User.Name -> ResolveDisplayName fallback) instead of empty when
  there's no override yet. Reviewer can see what they're overriding.
- SaveMemberName auto-clears the override when the typed name equals
  the resolved default — avoids creating a redundant Member.Name that
  duplicates User.Name.
…tead

The textbox had a fixed `width: 220px` which pushed the Save / Reset /
Cancel buttons off-screen on narrow columns or zoomed-out windows.

Switch the textbox to `flex: 1 1 0; min-width: 0;` so it absorbs
available space and shrinks freely. Buttons get `flex: 0 0 auto;` to
hold their natural size. Stack gets `flex-wrap: nowrap;` so the row
never breaks across lines either.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 29, 2026

@poxet poxet merged commit 0ad2f6b into master Apr 29, 2026
4 of 6 checks passed
@poxet poxet deleted the feature/team-member-name-edit branch April 29, 2026 12:44
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