Add live skill filtering and align tests with current campaign behavior

This commit is contained in:
2026-02-26 15:37:17 +01:00
parent 59fe453297
commit ba8141b336
8 changed files with 119 additions and 75 deletions

View File

@@ -44,6 +44,15 @@
<h3 class="skills-heading">@SelectedCharacter.Name <span
class="muted">| Owner: @OwnerLabel(SelectedCharacter.OwnerUserId) | Campaign: @SelectedCampaign.Name</span>
</h3>
<div class="skill-filter-wrap">
<label class="sr-only" for="skill-filter-input">Filter skills</label>
<input id="skill-filter-input"
class="skill-filter-input"
type="search"
placeholder="Filter skills"
@bind="SkillFilterText"
@bind:event="oninput"/>
</div>
<div class="chip-toolbar">
<label class="visibility-control" for="roll-visibility">Visibility</label>
<select id="roll-visibility" value="@(RollVisibility == "private" ? "private" : "public")" @onchange="OnRollVisibilityChangedAsync">
@@ -54,15 +63,22 @@
</div>
@{
var orderedSkillGroups = SelectedCharacterSkillGroups.OrderBy(group => group.Name).ToList();
var ungroupedSkills = SelectedCharacterSkills.Where(skill => !skill.SkillGroupId.HasValue).ToList();
var filteredSkills = SelectedCharacterSkills.Where(SkillMatchesFilter).ToList();
var hasSkillFilter = !string.IsNullOrWhiteSpace(SkillFilterText);
var visibleSkillGroups = orderedSkillGroups.Where(group => !hasSkillFilter || filteredSkills.Any(skill => skill.SkillGroupId == group.Id)).ToList();
var ungroupedSkills = filteredSkills.Where(skill => !skill.SkillGroupId.HasValue).ToList();
}
@if (SelectedCharacterSkills.Count == 0 && orderedSkillGroups.Count == 0)
@if (!hasSkillFilter && SelectedCharacterSkills.Count == 0 && orderedSkillGroups.Count == 0)
{
<p class="empty">No skills for this character yet.</p>
}
@foreach (var group in orderedSkillGroups)
@if (hasSkillFilter && filteredSkills.Count == 0)
{
var groupSkills = SelectedCharacterSkills.Where(skill => skill.SkillGroupId == group.Id).ToList();
<p class="empty">No skills match the current filter.</p>
}
@foreach (var group in visibleSkillGroups)
{
var groupSkills = filteredSkills.Where(skill => skill.SkillGroupId == group.Id).ToList();
<div class="skill-group-block">
<div class="skill-group-head">
<strong>@group.Name</strong>
@@ -87,7 +103,7 @@
</button>
</div>
</div>
@if (groupSkills.Count == 0)
@if (!hasSkillFilter && groupSkills.Count == 0)
{
<p class="empty">No skills in this group yet.</p>
}
@@ -141,63 +157,66 @@
</div>
</div>
}
<div class="skill-group-block">
<div class="skill-group-head">
<strong>Ungrouped</strong>
</div>
@if (ungroupedSkills.Count == 0)
{
<p class="empty">No ungrouped skills.</p>
}
<div class="skill-list">
@foreach (var skill in ungroupedSkills)
@if (!hasSkillFilter || ungroupedSkills.Count > 0)
{
<div class="skill-group-block">
<div class="skill-group-head">
<strong>Ungrouped</strong>
</div>
@if (!hasSkillFilter && ungroupedSkills.Count == 0)
{
<div class="skill-item">
<div class="skill-details">
<strong>@skill.Name</strong>
<span>@SkillDefinitionLabel(skill)</span>
</div>
<div class="skill-chip-actions">
<button
type="button"
class="chip-button"
title="Edit skill"
disabled="@(IsMutating || !CanEditSkill(skill))"
@onclick="() => OpenEditSkillModal(skill)">
<span aria-hidden="true" class="emoji">✏️</span>
<span class="sr-only">Edit @skill.Name</span>
</button>
<button
type="button"
class="chip-button"
title="Roll skill"
disabled="@(IsMutating)"
@onclick="() => RollSkillAsync(skill.Id)">
<span aria-hidden="true" class="emoji">🎲</span>
<span class="sr-only">Roll @skill.Name</span>
</button>
<button
type="button"
class="chip-button"
title="Delete skill"
disabled="@(IsMutating || !CanEditSkill(skill))"
@onclick="() => DeleteSkillAsync(skill.Id)">
<span aria-hidden="true" class="emoji">🗑️</span>
<span class="sr-only">Delete @skill.Name</span>
</button>
</div>
</div>
<p class="empty">No ungrouped skills.</p>
}
<button
type="button"
class="skill-item create-skill-item"
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
@onclick="() => OpenCreateSkillModal(null)">
<span class="skill-create-icon" aria-hidden="true">+</span>
<span>Add skill</span>
</button>
<div class="skill-list">
@foreach (var skill in ungroupedSkills)
{
<div class="skill-item">
<div class="skill-details">
<strong>@skill.Name</strong>
<span>@SkillDefinitionLabel(skill)</span>
</div>
<div class="skill-chip-actions">
<button
type="button"
class="chip-button"
title="Edit skill"
disabled="@(IsMutating || !CanEditSkill(skill))"
@onclick="() => OpenEditSkillModal(skill)">
<span aria-hidden="true" class="emoji">✏️</span>
<span class="sr-only">Edit @skill.Name</span>
</button>
<button
type="button"
class="chip-button"
title="Roll skill"
disabled="@(IsMutating)"
@onclick="() => RollSkillAsync(skill.Id)">
<span aria-hidden="true" class="emoji">🎲</span>
<span class="sr-only">Roll @skill.Name</span>
</button>
<button
type="button"
class="chip-button"
title="Delete skill"
disabled="@(IsMutating || !CanEditSkill(skill))"
@onclick="() => DeleteSkillAsync(skill.Id)">
<span aria-hidden="true" class="emoji">🗑️</span>
<span class="sr-only">Delete @skill.Name</span>
</button>
</div>
</div>
}
<button
type="button"
class="skill-item create-skill-item"
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
@onclick="() => OpenCreateSkillModal(null)">
<span class="skill-create-icon" aria-hidden="true">+</span>
<span>Add skill</span>
</button>
</div>
</div>
</div>
}
<button
type="button"

View File

@@ -221,6 +221,16 @@ public partial class CharacterPanel
}
}
private bool SkillMatchesFilter(SkillSummary skill)
{
if (string.IsNullOrWhiteSpace(SkillFilterText))
return true;
var filter = SkillFilterText.Trim();
return skill.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) ||
skill.DiceRollDefinition.Contains(filter, StringComparison.OrdinalIgnoreCase);
}
private static string InitialsFor(string value)
{
var words = value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
@@ -245,6 +255,7 @@ public partial class CharacterPanel
private int CreateSkillFormVersion { get; set; }
private int EditSkillFormVersion { get; set; }
private bool IsSubmittingSkillGroup { get; set; }
private string SkillFilterText { get; set; } = string.Empty;
[Inject]
private RpgRollerApiClient ApiClient { get; set; } = null!;

View File

@@ -422,11 +422,19 @@ select:focus-visible {
font-size: 1rem;
}
.skill-filter-wrap {
margin-left: auto;
}
.skill-filter-input {
width: min(15rem, 46vw);
padding: 0.2rem 0.35rem;
}
.chip-toolbar {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin-left: auto;
}
.visibility-control {