Cleanup campaign management ux
This commit is contained in:
@@ -31,9 +31,9 @@ internal static class CharacterEndpoints
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
group.MapGet("/characters/current-campaign", (HttpContext context, IGameService game) =>
|
||||
group.MapGet("/characters", (HttpContext context, IGameService game) =>
|
||||
{
|
||||
var result = game.GetCurrentCampaignCharacters(context.GetRequiredSessionToken());
|
||||
var result = game.GetOwnCharacters(context.GetRequiredSessionToken());
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
|
||||
@@ -9,27 +9,15 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<label for="campaign-select">Campaign</label>
|
||||
<label for="campaign-select">Current campaign</label>
|
||||
<select id="campaign-select" @onchange="CampaignSelectionChanged">
|
||||
@foreach (var campaign in Campaigns)
|
||||
{
|
||||
<option value="@campaign.Id" selected="@(campaign.Id == SelectedCampaignId)">@campaign.Name (@campaign.RulesetId)</option>
|
||||
<option value="@campaign.Id" selected="@(campaign.Id == SelectedCampaignId)">@campaign.Name (@campaign.RulesetId), GM: @campaign.Gm.DisplayName, @campaign.Characters.Count characters</option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
|
||||
@if (SelectedCampaign is null)
|
||||
{
|
||||
<p class="empty">No campaign selected.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>Name: <strong>@SelectedCampaign.Name</strong></p>
|
||||
<p>Ruleset: <strong>@SelectedCampaign.RulesetId</strong></p>
|
||||
<p>GM: <strong>@SelectedCampaign.Gm.DisplayName</strong> <span class="muted">(@SelectedCampaign.Gm.Username)</span></p>
|
||||
<p>Characters visible: <strong>@SelectedCampaign.Characters.Count</strong></p>
|
||||
}
|
||||
|
||||
<button type="button"
|
||||
class="add-row-button"
|
||||
disabled="@(IsMutating || IsCreatingCampaign)"
|
||||
|
||||
@@ -48,7 +48,7 @@ public partial class CampaignManagementPanel
|
||||
IsCreatingCampaign = true;
|
||||
try
|
||||
{
|
||||
var campaign = await ApiClient.RequestAsync<CampaignSummary>("POST", "/api/campaigns", new CreateCampaignRequest(CampaignState.Model.Name.Trim(), CampaignState.Model.RulesetId));
|
||||
var campaign = await ApiClient.RequestAsync<CampaignDetails>("POST", "/api/campaigns", new CreateCampaignRequest(CampaignState.Model.Name.Trim(), CampaignState.Model.RulesetId));
|
||||
|
||||
CampaignState.Model.Name = string.Empty;
|
||||
ShowCreateCampaignModal = false;
|
||||
@@ -72,7 +72,7 @@ public partial class CampaignManagementPanel
|
||||
private bool ShowCreateCampaignModal { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public IReadOnlyList<CampaignSummary> Campaigns { get; set; } = [];
|
||||
public IReadOnlyList<CampaignDetails> Campaigns { get; set; } = [];
|
||||
|
||||
[Parameter]
|
||||
public Guid? SelectedCampaignId { get; set; }
|
||||
|
||||
@@ -96,7 +96,7 @@ public partial class CharacterFormModal
|
||||
public Guid? EditingCharacterId { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public IReadOnlyList<CampaignSummary> Campaigns { get; set; } = [];
|
||||
public IReadOnlyList<CampaignDetails> Campaigns { get; set; } = [];
|
||||
|
||||
[Parameter]
|
||||
public bool IsMutating { get; set; }
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
type="button"
|
||||
class="chip-button"
|
||||
title="Roll skill"
|
||||
disabled="@(IsMutating || !CanRollSkill(skill))"
|
||||
disabled="@(IsMutating)"
|
||||
@onclick="() => RollSkillAsync(skill.Id)">
|
||||
<span aria-hidden="true">⚄</span>
|
||||
<span class="sr-only">Roll @skill.Name</span>
|
||||
@@ -171,7 +171,7 @@
|
||||
type="button"
|
||||
class="chip-button"
|
||||
title="Roll skill"
|
||||
disabled="@(IsMutating || !CanRollSkill(skill))"
|
||||
disabled="@(IsMutating)"
|
||||
@onclick="() => RollSkillAsync(skill.Id)">
|
||||
<span aria-hidden="true">⚄</span>
|
||||
<span class="sr-only">Roll @skill.Name</span>
|
||||
@@ -203,7 +203,9 @@
|
||||
type="button"
|
||||
class="add-row-button"
|
||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||
@onclick="OpenCreateSkillGroupModal">[+] Add group
|
||||
@onclick="OpenCreateSkillGroupModal">
|
||||
<span class="skill-create-icon" aria-hidden="true">+</span>
|
||||
<span>Add group</span>
|
||||
</button>
|
||||
</article>
|
||||
}
|
||||
|
||||
@@ -291,9 +291,6 @@ public partial class CharacterPanel
|
||||
[Parameter]
|
||||
public Func<SkillSummary, bool> CanEditSkill { get; set; } = _ => false;
|
||||
|
||||
[Parameter]
|
||||
public Func<SkillSummary, bool> CanRollSkill { get; set; } = _ => false;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<Guid> CharacterSelected { get; set; }
|
||||
|
||||
|
||||
@@ -78,7 +78,6 @@
|
||||
SkillDefinitionLabel="SkillDefinitionLabel"
|
||||
CanEditCharacter="CanEditCharacter"
|
||||
CanEditSkill="CanEditSkill"
|
||||
CanRollSkill="CanRollSkill"
|
||||
CharacterSelected="SelectCharacterAsync"
|
||||
EditCharacterRequested="OpenEditCharacterModal"
|
||||
SkillCreated="OnSkillCreatedAsync"
|
||||
|
||||
@@ -136,7 +136,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
|
||||
private async Task ReloadCampaignsAsync(Guid? preferredCampaignId)
|
||||
{
|
||||
var campaigns = await ApiClient.RequestAsync<IReadOnlyList<CampaignSummary>>("GET", "/api/campaigns");
|
||||
var campaigns = await ApiClient.RequestAsync<IReadOnlyList<CampaignDetails>>("GET", "/api/campaigns");
|
||||
Campaigns = campaigns.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
if (Campaigns.Count == 0)
|
||||
@@ -191,30 +191,6 @@ public partial class Workspace : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ManualRefreshAsync()
|
||||
{
|
||||
if (IsMutating)
|
||||
return;
|
||||
|
||||
IsMutating = true;
|
||||
try
|
||||
{
|
||||
await CheckHealthAsync();
|
||||
var reloaded = await ReloadAuthenticatedSessionAsync(SelectedCampaignId);
|
||||
if (!reloaded)
|
||||
{
|
||||
await LoggedOut.InvokeAsync("Session expired. Please log in again.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetStatus("Campaign data refreshed.", false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsMutating = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LogoutAsync()
|
||||
{
|
||||
if (IsMutating)
|
||||
@@ -435,23 +411,10 @@ public partial class Workspace : IAsyncDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedSkill = SelectedCampaign.Skills.FirstOrDefault(skill => skill.Id == skillId);
|
||||
if (selectedSkill is null)
|
||||
{
|
||||
SetStatus("Skill is no longer available. Refresh campaign data.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CanRollSkill(selectedSkill))
|
||||
{
|
||||
SetStatus("You are not allowed to roll this skill.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
IsMutating = true;
|
||||
try
|
||||
{
|
||||
LastRoll = await ApiClient.RequestAsync<RollResult>("POST", $"/api/skills/{selectedSkill.Id}/roll", new RollSkillRequest(RollVisibility));
|
||||
LastRoll = await ApiClient.RequestAsync<RollResult>("POST", $"/api/skills/{skillId}/roll", new RollSkillRequest(RollVisibility));
|
||||
|
||||
await RefreshCampaignScopeAsync();
|
||||
SetStatus("Roll recorded.", false);
|
||||
@@ -487,11 +450,6 @@ public partial class Workspace : IAsyncDisposable
|
||||
return character is not null && CanEditCharacter(character);
|
||||
}
|
||||
|
||||
private bool CanRollSkill(SkillSummary skill)
|
||||
{
|
||||
return CanEditSkill(skill);
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public async Task OnStateEventReceived(long _)
|
||||
{
|
||||
@@ -736,7 +694,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
private Guid? ActiveCharacterId { get; set; }
|
||||
private Guid? SelectedCampaignId { get; set; }
|
||||
private CampaignDetails? SelectedCampaign { get; set; }
|
||||
private List<CampaignSummary> Campaigns { get; set; } = [];
|
||||
private List<CampaignDetails> Campaigns { get; set; } = [];
|
||||
private List<CampaignLogEntry> CampaignLog { get; set; } = [];
|
||||
private List<RulesetDefinition> Rulesets { get; set; } = [];
|
||||
private Guid? SelectedCharacterId { get; set; }
|
||||
|
||||
@@ -16,8 +16,6 @@ public sealed record RulesetDefinition(string Id, string Name, string DiceSyntax
|
||||
|
||||
public sealed record CreateCampaignRequest(string Name, string RulesetId);
|
||||
|
||||
public sealed record CampaignSummary(Guid Id, string Name, string RulesetId, Guid GmUserId);
|
||||
|
||||
public sealed record CampaignDetails(Guid Id, string Name, string RulesetId, UserSummary Gm, IReadOnlyList<CharacterSummary> Characters, IReadOnlyList<SkillGroupSummary> SkillGroups, IReadOnlyList<SkillSummary> Skills);
|
||||
|
||||
public sealed record CreateCharacterRequest(string Name, Guid CampaignId);
|
||||
|
||||
@@ -127,20 +127,20 @@ public sealed class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<CampaignSummary> CreateCampaign(string sessionToken, string name, string rulesetId)
|
||||
public ServiceResult<CampaignDetails> CreateCampaign(string sessionToken, string name, string rulesetId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return ServiceResult<CampaignSummary>.Failure("invalid_campaign_name", "Campaign name is required.");
|
||||
return ServiceResult<CampaignDetails>.Failure("invalid_campaign_name", "Campaign name is required.");
|
||||
|
||||
var ruleset = DiceRules.TryParseRulesetId(rulesetId);
|
||||
if (ruleset is null)
|
||||
return ServiceResult<CampaignSummary>.Failure("invalid_ruleset", "Unknown ruleset.");
|
||||
return ServiceResult<CampaignDetails>.Failure("invalid_ruleset", "Unknown ruleset.");
|
||||
|
||||
lock (m_Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<CampaignSummary>.Failure("unauthorized", "You must be logged in.");
|
||||
return ServiceResult<CampaignDetails>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
var campaign = new Campaign
|
||||
{
|
||||
@@ -153,25 +153,25 @@ public sealed class GameService : IGameService
|
||||
|
||||
m_CampaignsById[campaign.Id] = campaign;
|
||||
PersistStateLocked();
|
||||
return ServiceResult<CampaignSummary>.Success(ToCampaignSummary(campaign));
|
||||
return ServiceResult<CampaignDetails>.Success(ToCampaignDetails(campaign));
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<IReadOnlyList<CampaignSummary>> GetCampaigns(string sessionToken)
|
||||
public ServiceResult<IReadOnlyList<CampaignDetails>> GetCampaigns(string sessionToken)
|
||||
{
|
||||
lock (m_Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<IReadOnlyList<CampaignSummary>>.Failure("unauthorized", "You must be logged in.");
|
||||
return ServiceResult<IReadOnlyList<CampaignDetails>>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
var campaignIds = new HashSet<Guid>(m_CampaignsById.Values.Where(c => c.GmUserId == user.Id).Select(c => c.Id));
|
||||
foreach (var character in m_CharactersById.Values.Where(c => c.OwnerUserId == user.Id))
|
||||
campaignIds.Add(character.CampaignId);
|
||||
|
||||
var results = campaignIds.Select(id => m_CampaignsById[id]).OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).Select(ToCampaignSummary).ToArray();
|
||||
var results = campaignIds.Select(id => m_CampaignsById[id]).OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).Select(ToCampaignDetails).ToArray();
|
||||
|
||||
return ServiceResult<IReadOnlyList<CampaignSummary>>.Success(results);
|
||||
return ServiceResult<IReadOnlyList<CampaignDetails>>.Success(results);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,11 +183,9 @@ public sealed class GameService : IGameService
|
||||
if (!context.Succeeded)
|
||||
return ServiceResult<CampaignDetails>.Failure(context.Error!.Code, context.Error.Message);
|
||||
|
||||
var (user, campaign) = context.Value!;
|
||||
var (_, campaign) = context.Value;
|
||||
var gm = m_UsersById[campaign.GmUserId];
|
||||
var isGm = campaign.GmUserId == user.Id;
|
||||
|
||||
var characters = m_CharactersById.Values.Where(c => c.CampaignId == campaign.Id).Where(c => isGm || c.OwnerUserId == user.Id).OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).Select(ToCharacterSummary).ToArray();
|
||||
var characters = m_CharactersById.Values.Where(c => c.CampaignId == campaign.Id).Select(ToCharacterSummary).ToList();
|
||||
|
||||
var visibleCharacterIds = characters.Select(c => c.Id).ToHashSet();
|
||||
|
||||
@@ -318,7 +316,7 @@ public sealed class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<IReadOnlyList<CharacterSummary>> GetCurrentCampaignCharacters(string sessionToken)
|
||||
public ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken)
|
||||
{
|
||||
lock (m_Gate)
|
||||
{
|
||||
@@ -326,10 +324,7 @@ public sealed class GameService : IGameService
|
||||
if (user is null)
|
||||
return ServiceResult<IReadOnlyList<CharacterSummary>>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!TryGetCurrentCampaignIdLocked(user, out var campaignId))
|
||||
return ServiceResult<IReadOnlyList<CharacterSummary>>.Failure("no_active_character", "No active character is selected.");
|
||||
|
||||
var characters = m_CharactersById.Values.Where(c => c.CampaignId == campaignId && c.OwnerUserId == user.Id).OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).Select(ToCharacterSummary).ToArray();
|
||||
var characters = m_CharactersById.Values.Where(c => c.OwnerUserId == user.Id).OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).Select(ToCharacterSummary).ToArray();
|
||||
|
||||
return ServiceResult<IReadOnlyList<CharacterSummary>>.Success(characters);
|
||||
}
|
||||
@@ -803,9 +798,18 @@ public sealed class GameService : IGameService
|
||||
return new(user.Id, user.Username, user.DisplayName);
|
||||
}
|
||||
|
||||
private static CampaignSummary ToCampaignSummary(Campaign campaign)
|
||||
private CampaignDetails ToCampaignDetails(Campaign campaign)
|
||||
{
|
||||
return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), campaign.GmUserId);
|
||||
lock (m_Gate)
|
||||
{
|
||||
var gm = m_UsersById[campaign.GmUserId];
|
||||
var characters = m_CharactersById.Values.Where(c => c.CampaignId == campaign.Id).Select(ToCharacterSummary).ToList();
|
||||
var visibleCharacterIds = characters.Select(c => c.Id).ToHashSet();
|
||||
var skillGroups = m_SkillGroupsById.Values.Where(g => visibleCharacterIds.Contains(g.CharacterId)).OrderBy(g => g.Name, StringComparer.OrdinalIgnoreCase).Select(ToSkillGroupSummary).ToArray();
|
||||
var skills = m_SkillsById.Values.Where(s => visibleCharacterIds.Contains(s.CharacterId)).OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase).Select(ToSkillSummary).ToArray();
|
||||
|
||||
return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), ToUserSummary(gm), characters, skillGroups, skills);
|
||||
}
|
||||
}
|
||||
|
||||
private static CharacterSummary ToCharacterSummary(Character character)
|
||||
|
||||
@@ -12,15 +12,15 @@ public interface IGameService
|
||||
UserSummary? GetUserBySession(string sessionToken);
|
||||
ServiceResult<MeResponse> GetMe(string sessionToken);
|
||||
|
||||
ServiceResult<CampaignSummary> CreateCampaign(string sessionToken, string name, string rulesetId);
|
||||
ServiceResult<IReadOnlyList<CampaignSummary>> GetCampaigns(string sessionToken);
|
||||
ServiceResult<CampaignDetails> CreateCampaign(string sessionToken, string name, string rulesetId);
|
||||
ServiceResult<IReadOnlyList<CampaignDetails>> GetCampaigns(string sessionToken);
|
||||
ServiceResult<CampaignDetails> GetCampaign(string sessionToken, Guid campaignId);
|
||||
ServiceResult<IReadOnlyList<string>> GetUsernames(string sessionToken);
|
||||
|
||||
ServiceResult<CharacterSummary> CreateCharacter(string sessionToken, string name, Guid campaignId);
|
||||
ServiceResult<CharacterSummary> UpdateCharacter(string sessionToken, Guid characterId, string name, Guid campaignId, string? ownerUsername = null);
|
||||
ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId);
|
||||
ServiceResult<IReadOnlyList<CharacterSummary>> GetCurrentCampaignCharacters(string sessionToken);
|
||||
ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken);
|
||||
|
||||
ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble);
|
||||
ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble);
|
||||
|
||||
Reference in New Issue
Block a user