Add owner/admin character deletion in campaign management
This commit is contained in:
@@ -19,6 +19,12 @@ internal static class CharacterEndpoints
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
group.MapDelete("/characters/{characterId:guid}", (Guid characterId, HttpContext context, IGameService game) =>
|
||||
{
|
||||
var result = game.DeleteCharacter(context.GetRequiredSessionToken(), characterId);
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
group.MapPost("/characters/{characterId:guid}/activate", (Guid characterId, HttpContext context, IGameService game) =>
|
||||
{
|
||||
var result = game.ActivateCharacter(context.GetRequiredSessionToken(), characterId);
|
||||
|
||||
@@ -58,14 +58,24 @@
|
||||
<strong>@character.Name</strong>
|
||||
<p class="muted">Owner: @OwnerLabel(character.OwnerUserId)</p>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="chip-button"
|
||||
title="Edit character"
|
||||
disabled="@(IsMutating || IsCreatingCampaign || !CanEditCharacter(character))"
|
||||
@onclick="() => EditCharacterRequested.InvokeAsync(character)">
|
||||
<span aria-hidden="true" class="emoji">✏️</span>
|
||||
<span class="sr-only">Edit @character.Name</span>
|
||||
</button>
|
||||
<div class="management-actions">
|
||||
<button type="button"
|
||||
class="chip-button"
|
||||
title="Edit character"
|
||||
disabled="@(IsMutating || IsCreatingCampaign || !CanEditCharacter(character))"
|
||||
@onclick="() => EditCharacterRequested.InvokeAsync(character)">
|
||||
<span aria-hidden="true" class="emoji">✏️</span>
|
||||
<span class="sr-only">Edit @character.Name</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="chip-button"
|
||||
title="Delete character"
|
||||
disabled="@(IsMutating || IsCreatingCampaign || !CanDeleteCharacter(character))"
|
||||
@onclick="() => DeleteCharacterRequested.InvokeAsync(character)">
|
||||
<span aria-hidden="true" class="emoji">🗑️</span>
|
||||
<span class="sr-only">Delete @character.Name</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -92,6 +92,9 @@ public partial class CampaignManagementPanel
|
||||
[Parameter]
|
||||
public Func<CharacterSummary, bool> CanEditCharacter { get; set; } = _ => false;
|
||||
|
||||
[Parameter]
|
||||
public Func<CharacterSummary, bool> CanDeleteCharacter { get; set; } = _ => false;
|
||||
|
||||
[Parameter]
|
||||
public bool CanDeleteCampaign { get; set; }
|
||||
|
||||
@@ -109,4 +112,7 @@ public partial class CampaignManagementPanel
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<CharacterSummary> EditCharacterRequested { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<CharacterSummary> DeleteCharacterRequested { get; set; }
|
||||
}
|
||||
|
||||
@@ -127,12 +127,14 @@
|
||||
IsMutating="IsMutating"
|
||||
OwnerLabel="OwnerLabel"
|
||||
CanEditCharacter="CanEditCharacter"
|
||||
CanDeleteCharacter="CanDeleteCharacter"
|
||||
CanDeleteCampaign="CanDeleteSelectedCampaign"
|
||||
CampaignSelectionChanged="OnCampaignSelectionChangedAsync"
|
||||
CampaignCreated="OnCampaignCreatedAsync"
|
||||
DeleteCampaignRequested="DeleteSelectedCampaignAsync"
|
||||
CreateCharacterRequested="OpenCreateCharacterModal"
|
||||
EditCharacterRequested="OpenEditCharacterModal"/>
|
||||
EditCharacterRequested="OpenEditCharacterModal"
|
||||
DeleteCharacterRequested="DeleteCharacterAsync"/>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -371,6 +371,35 @@ public partial class Workspace : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteCharacterAsync(CharacterSummary character)
|
||||
{
|
||||
if (IsMutating || !CanDeleteCharacter(character))
|
||||
return;
|
||||
|
||||
var confirmed = await JS.InvokeAsync<bool>("confirm", $"Delete character '{character.Name}'?");
|
||||
if (!confirmed)
|
||||
return;
|
||||
|
||||
IsMutating = true;
|
||||
try
|
||||
{
|
||||
_ = await ApiClient.RequestAsync<bool>("DELETE", $"/api/characters/{character.Id}");
|
||||
await ReloadCampaignsAsync(SelectedCampaignId);
|
||||
await ReloadCharacterCampaignOptionsAsync();
|
||||
await RefreshCampaignScopeAsync();
|
||||
await SyncStateEventsAsync();
|
||||
SetStatus("Character deleted.", false);
|
||||
}
|
||||
catch (ApiRequestException ex)
|
||||
{
|
||||
SetStatus(ex.Message, true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsMutating = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SelectCharacterAsync(Guid characterId)
|
||||
{
|
||||
SelectedCharacterId = characterId;
|
||||
@@ -382,6 +411,11 @@ public partial class Workspace : IAsyncDisposable
|
||||
return User is not null && (character.OwnerUserId == User.Id || IsCurrentUserGm || IsCurrentUserAdmin);
|
||||
}
|
||||
|
||||
private bool CanDeleteCharacter(CharacterSummary character)
|
||||
{
|
||||
return User is not null && (character.OwnerUserId == User.Id || IsCurrentUserAdmin);
|
||||
}
|
||||
|
||||
private static bool CanActivateCharacter(CharacterSummary character, UserSummary? user)
|
||||
{
|
||||
return user is not null && character.OwnerUserId == user.Id;
|
||||
|
||||
@@ -432,6 +432,28 @@ public sealed class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<bool> DeleteCharacter(string sessionToken, Guid characterId)
|
||||
{
|
||||
lock (m_Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!m_CharactersById.TryGetValue(characterId, out var character))
|
||||
return ServiceResult<bool>.Failure("character_not_found", "Character was not found.");
|
||||
|
||||
var isOwner = character.OwnerUserId == user.Id;
|
||||
var isAdmin = UserHasRoleLocked(user, UserRoles.Admin);
|
||||
if (!isOwner && !isAdmin)
|
||||
return ServiceResult<bool>.Failure("forbidden", "Only the owner or admin can delete this character.");
|
||||
|
||||
DeleteCharacterLocked(characterId);
|
||||
PersistStateLocked();
|
||||
return ServiceResult<bool>.Success(true);
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId)
|
||||
{
|
||||
lock (m_Gate)
|
||||
|
||||
@@ -24,6 +24,7 @@ public interface IGameService
|
||||
|
||||
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> DeleteCharacter(string sessionToken, Guid characterId);
|
||||
ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId);
|
||||
ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken);
|
||||
|
||||
|
||||
@@ -705,6 +705,12 @@ select:focus-visible {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.management-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.add-row-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user