From 6f94b1ba95c74cafeacfc32d6d873916b448a9db Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Thu, 26 Feb 2026 17:42:44 +0100 Subject: [PATCH] Fix campaign owner labels to always use display names --- README.md | 1 + RpgRoller.Tests/Api/CampaignApiTests.cs | 8 ++++++++ RpgRoller/Components/Pages/Workspace.razor.cs | 12 ++++++++++-- RpgRoller/Contracts/ApiContracts.cs | 2 +- RpgRoller/Services/GameService.cs | 12 ++++++++++-- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 63f7cd9..9ea1ddd 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Gameplay capabilities now include: - Character owner selection in edit modal backed by existing-username dropdown data - Role-aware authorization with admin role support (including admin user/role management) - Campaign deletion by campaign owner or admin (unlinks characters from the campaign and clears campaign log entries) +- Campaign management owner labels use account display names (no GUID fallback rendering) ## Prerequisites diff --git a/RpgRoller.Tests/Api/CampaignApiTests.cs b/RpgRoller.Tests/Api/CampaignApiTests.cs index d2fad7f..23222db 100644 --- a/RpgRoller.Tests/Api/CampaignApiTests.cs +++ b/RpgRoller.Tests/Api/CampaignApiTests.cs @@ -18,6 +18,7 @@ public sealed class CampaignApiTests : ApiTestBase var campaign = await PostAsync(gmClient, "/api/campaigns", new("Alpha Campaign", "dnd5e")); var gmCharacter = await PostAsync(gmClient, "/api/characters", new("Arin", campaign.Id)); + Assert.Equal("Game Master", gmCharacter.OwnerDisplayName); var activateResponse = await gmClient.PostAsync($"/api/characters/{gmCharacter.Id}/activate", null); Assert.Equal(HttpStatusCode.OK, activateResponse.StatusCode); @@ -39,10 +40,12 @@ public sealed class CampaignApiTests : ApiTestBase var details = await GetAsync(gmClient, $"/api/campaigns/{campaign.Id}"); Assert.Equal(campaign.Id, details.Id); Assert.Single(details.Characters); + Assert.Equal("Game Master", details.Characters[0].OwnerDisplayName); var currentCampaignCharacters = await GetAsync>(gmClient, "/api/characters"); Assert.Single(currentCampaignCharacters); Assert.Equal(gmCharacter.Id, currentCampaignCharacters[0].Id); + Assert.Equal("Game Master", currentCampaignCharacters[0].OwnerDisplayName); var otherCampaign = await PostAsync(gmClient, "/api/campaigns", new("Beta Campaign", "d6")); @@ -96,6 +99,11 @@ public sealed class CampaignApiTests : ApiTestBase var transferResult = await PutAsync(gmClient, $"/api/characters/{character.Id}", new("Grouped Hero", campaign.Id, "receiver2")); Assert.Equal("Grouped Hero", transferResult.Name); + Assert.Equal("Receiver", transferResult.OwnerDisplayName); + + var gmCampaignView = await GetAsync(gmClient, $"/api/campaigns/{campaign.Id}"); + var gmViewedCharacter = Assert.Single(gmCampaignView.Characters, c => c.Id == character.Id); + Assert.Equal("Receiver", gmViewedCharacter.OwnerDisplayName); var ownerActivate = await ownerClient.PostAsync($"/api/characters/{character.Id}/activate", null); Assert.Equal(HttpStatusCode.BadRequest, ownerActivate.StatusCode); diff --git a/RpgRoller/Components/Pages/Workspace.razor.cs b/RpgRoller/Components/Pages/Workspace.razor.cs index 4edb579..2b2b4aa 100644 --- a/RpgRoller/Components/Pages/Workspace.razor.cs +++ b/RpgRoller/Components/Pages/Workspace.razor.cs @@ -590,10 +590,18 @@ public partial class Workspace : IAsyncDisposable if (User is not null && ownerUserId == User.Id) return "You"; - if (SelectedCampaign is not null && ownerUserId == SelectedCampaign.Gm.Id) + if (SelectedCampaign is null) + return "Unknown owner"; + + if (ownerUserId == SelectedCampaign.Gm.Id) return $"{SelectedCampaign.Gm.DisplayName} (GM)"; - return ownerUserId.ToString("N")[..8]; + var ownerDisplayName = SelectedCampaign.Characters + .Where(character => character.OwnerUserId == ownerUserId) + .Select(character => character.OwnerDisplayName) + .FirstOrDefault(displayName => !string.IsNullOrWhiteSpace(displayName)); + + return string.IsNullOrWhiteSpace(ownerDisplayName) ? "Unknown owner" : ownerDisplayName; } private string CharacterLabel(Guid characterId) diff --git a/RpgRoller/Contracts/ApiContracts.cs b/RpgRoller/Contracts/ApiContracts.cs index 5e27fc9..5a18d8d 100644 --- a/RpgRoller/Contracts/ApiContracts.cs +++ b/RpgRoller/Contracts/ApiContracts.cs @@ -26,7 +26,7 @@ public sealed record CreateCharacterRequest(string Name, Guid CampaignId); public sealed record UpdateCharacterRequest(string Name, Guid CampaignId, string? OwnerUsername = null); -public sealed record CharacterSummary(Guid Id, string Name, Guid OwnerUserId, Guid? CampaignId); +public sealed record CharacterSummary(Guid Id, string Name, Guid OwnerUserId, Guid? CampaignId, string OwnerDisplayName); public sealed record CreateSkillRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, Guid? SkillGroupId = null); diff --git a/RpgRoller/Services/GameService.cs b/RpgRoller/Services/GameService.cs index 63009c6..5b2caa5 100644 --- a/RpgRoller/Services/GameService.cs +++ b/RpgRoller/Services/GameService.cs @@ -948,9 +948,10 @@ public sealed class GameService : IGameService } } - private static CharacterSummary ToCharacterSummary(Character character) + private CharacterSummary ToCharacterSummary(Character character) { - return new(character.Id, character.Name, character.OwnerUserId, character.CampaignId); + var ownerDisplayName = ResolveOwnerDisplayName(character.OwnerUserId); + return new(character.Id, character.Name, character.OwnerUserId, character.CampaignId, ownerDisplayName); } private static SkillGroupSummary ToSkillGroupSummary(SkillGroup skillGroup) @@ -980,6 +981,13 @@ public sealed class GameService : IGameService return JsonSerializer.Serialize(dice, DiceJsonOptions); } + private string ResolveOwnerDisplayName(Guid ownerUserId) + { + return m_UsersById.TryGetValue(ownerUserId, out var owner) && !string.IsNullOrWhiteSpace(owner.DisplayName) + ? owner.DisplayName + : "Unknown owner"; + } + private static IReadOnlyList DeserializeDice(string serializedDice) { if (string.IsNullOrWhiteSpace(serializedDice))