Extract shared header and skill group block components
This commit is contained in:
@@ -56,6 +56,7 @@ Gameplay capabilities now include:
|
|||||||
- Campaign management owner labels use account display names (no GUID fallback rendering)
|
- Campaign management owner labels use account display names (no GUID fallback rendering)
|
||||||
- Character edit flow supports unlinking from campaigns (owner/GM/admin) and assigning to any existing campaign via expanded campaign options
|
- Character edit flow supports unlinking from campaigns (owner/GM/admin) and assigning to any existing campaign via expanded campaign options
|
||||||
- Campaign management supports character deletion by character owner or admin
|
- Campaign management supports character deletion by character owner or admin
|
||||||
|
- Shared top header control across workspace and admin views (consistent navigation/logout behavior)
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,27 @@
|
|||||||
<div class="rr-app">
|
<div class="rr-app">
|
||||||
|
<div class="workspace-shell">
|
||||||
|
<AppHeader
|
||||||
|
User="CurrentUser"
|
||||||
|
ShowCampaign="false"
|
||||||
|
ShowConnectionState="true"
|
||||||
|
ConnectionStateLabel="@(!IsLoading && CurrentUser is not null ? "Connected" : "Offline fallback")"
|
||||||
|
ConnectionStateCssClass="@(!IsLoading && CurrentUser is not null ? "ok" : "offline")"
|
||||||
|
IsMenuOpen="IsScreenMenuOpen"
|
||||||
|
MenuButtonId="admin-screen-menu-button"
|
||||||
|
MenuId="admin-screen-menu"
|
||||||
|
MenuItems="HeaderMenuItems"
|
||||||
|
ToggleMenuRequested="ToggleScreenMenu"
|
||||||
|
LogoutRequested="LogoutAsync"/>
|
||||||
|
|
||||||
<main class="management-screen">
|
<main class="management-screen">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h2>Admin</h2>
|
<h2>User Management</h2>
|
||||||
</div>
|
|
||||||
@if (CurrentUser is null)
|
|
||||||
{
|
|
||||||
<p class="empty">Loading admin session...</p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<p class="muted"><strong>@CurrentUser.DisplayName</strong> (@CurrentUser.Username)</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="inline-actions">
|
|
||||||
<button type="button" class="ghost" disabled="@(IsMutating || IsLoading)" @onclick="BackToWorkspaceAsync">Back to workspace</button>
|
|
||||||
<a href="" class="logout-link" @onclick:preventDefault="true" @onclick="LogoutAsync">Logout</a>
|
|
||||||
</div>
|
</div>
|
||||||
@if (!string.IsNullOrWhiteSpace(StatusMessage))
|
@if (!string.IsNullOrWhiteSpace(StatusMessage))
|
||||||
{
|
{
|
||||||
<p class="@(StatusIsError ? "form-error" : "muted")">@StatusMessage</p>
|
<p class="@(StatusIsError ? "form-error" : "muted")">@StatusMessage</p>
|
||||||
}
|
}
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card">
|
|
||||||
<div class="section-head">
|
|
||||||
<h2>User Management</h2>
|
|
||||||
</div>
|
|
||||||
@if (IsLoading)
|
@if (IsLoading)
|
||||||
{
|
{
|
||||||
<p class="empty">Loading users...</p>
|
<p class="empty">Loading users...</p>
|
||||||
@@ -44,7 +39,6 @@
|
|||||||
<ul class="management-list">
|
<ul class="management-list">
|
||||||
@foreach (var user in Users)
|
@foreach (var user in Users)
|
||||||
{
|
{
|
||||||
var userIsAdmin = HasAdminRole(user);
|
|
||||||
<li>
|
<li>
|
||||||
<div>
|
<div>
|
||||||
<strong>@user.Username</strong>
|
<strong>@user.Username</strong>
|
||||||
@@ -73,4 +67,5 @@
|
|||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,11 +46,18 @@ public partial class AdminHome
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task BackToWorkspaceAsync()
|
private async Task OpenWorkspaceAsync()
|
||||||
{
|
{
|
||||||
|
IsScreenMenuOpen = false;
|
||||||
await WorkspaceRequested.InvokeAsync();
|
await WorkspaceRequested.InvokeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task OpenAdminAsync()
|
||||||
|
{
|
||||||
|
IsScreenMenuOpen = false;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LogoutAsync()
|
private async Task LogoutAsync()
|
||||||
{
|
{
|
||||||
if (IsMutating)
|
if (IsMutating)
|
||||||
@@ -148,6 +155,11 @@ public partial class AdminHome
|
|||||||
StatusIsError = isError;
|
StatusIsError = isError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ToggleScreenMenu()
|
||||||
|
{
|
||||||
|
IsScreenMenuOpen = !IsScreenMenuOpen;
|
||||||
|
}
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private RpgRollerApiClient ApiClient { get; set; } = null!;
|
private RpgRollerApiClient ApiClient { get; set; } = null!;
|
||||||
|
|
||||||
@@ -156,11 +168,23 @@ public partial class AdminHome
|
|||||||
|
|
||||||
private bool IsLoading { get; set; } = true;
|
private bool IsLoading { get; set; } = true;
|
||||||
private bool IsMutating { get; set; }
|
private bool IsMutating { get; set; }
|
||||||
|
private bool IsScreenMenuOpen { get; set; }
|
||||||
private bool IsCurrentUserAdmin { get; set; }
|
private bool IsCurrentUserAdmin { get; set; }
|
||||||
private UserSummary? CurrentUser { get; set; }
|
private UserSummary? CurrentUser { get; set; }
|
||||||
private List<AdminUserSummary> Users { get; set; } = [];
|
private List<AdminUserSummary> Users { get; set; } = [];
|
||||||
private string? StatusMessage { get; set; }
|
private string? StatusMessage { get; set; }
|
||||||
private bool StatusIsError { get; set; }
|
private bool StatusIsError { get; set; }
|
||||||
|
private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new AppHeaderMenuItem { Label = "Workspace", IsActive = false, OnSelected = OpenWorkspaceAsync },
|
||||||
|
new AppHeaderMenuItem { Label = "Admin", IsActive = true, OnSelected = OpenAdminAsync }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<string?> LoggedOut { get; set; }
|
public EventCallback<string?> LoggedOut { get; set; }
|
||||||
|
|||||||
53
RpgRoller/Components/Pages/HomeControls/AppHeader.razor
Normal file
53
RpgRoller/Components/Pages/HomeControls/AppHeader.razor
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<header class="workspace-header">
|
||||||
|
<div class="header-row">
|
||||||
|
<h1>@Title</h1>
|
||||||
|
@if (User is null)
|
||||||
|
{
|
||||||
|
<p class="header-identity"><strong>Loading user...</strong></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p class="header-identity"><strong>@User.DisplayName</strong> <span class="muted">(@User.Username)</span></p>
|
||||||
|
}
|
||||||
|
@if (ShowCampaign)
|
||||||
|
{
|
||||||
|
<p class="header-campaign">Campaign: <strong>@(CampaignName ?? "No campaign selected")</strong></p>
|
||||||
|
}
|
||||||
|
@if (ShowConnectionState)
|
||||||
|
{
|
||||||
|
<div class="header-connection-cell">
|
||||||
|
<p class="connection @ConnectionStateCssClass">@ConnectionStateLabel</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<a href="" class="logout-link" @onclick:preventDefault="true" @onclick="LogoutRequested">Logout</a>
|
||||||
|
@if (MenuItems.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="header-menu-wrap">
|
||||||
|
<button
|
||||||
|
id="@MenuButtonId"
|
||||||
|
type="button"
|
||||||
|
class="menu-toggle"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="@IsMenuOpen"
|
||||||
|
aria-controls="@MenuId"
|
||||||
|
@onclick="ToggleMenuRequested">
|
||||||
|
<span aria-hidden="true">☰</span>
|
||||||
|
</button>
|
||||||
|
@if (IsMenuOpen)
|
||||||
|
{
|
||||||
|
<div id="@MenuId" class="screen-menu" role="menu" aria-labelledby="@MenuButtonId">
|
||||||
|
@foreach (var item in MenuItems)
|
||||||
|
{
|
||||||
|
<button type="button"
|
||||||
|
class="menu-item @(item.IsActive ? "active" : string.Empty)"
|
||||||
|
role="menuitem"
|
||||||
|
@onclick="() => SelectMenuItemAsync(item)">
|
||||||
|
@item.Label
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
60
RpgRoller/Components/Pages/HomeControls/AppHeader.razor.cs
Normal file
60
RpgRoller/Components/Pages/HomeControls/AppHeader.razor.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
|
namespace RpgRoller.Components.Pages.HomeControls;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public partial class AppHeader
|
||||||
|
{
|
||||||
|
private Task SelectMenuItemAsync(AppHeaderMenuItem item)
|
||||||
|
{
|
||||||
|
return item.OnSelected?.Invoke() ?? Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Title { get; set; } = "RpgRoller";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public UserSummary? User { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowCampaign { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? CampaignName { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowConnectionState { get; set; } = true;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string ConnectionStateLabel { get; set; } = "Offline fallback";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string ConnectionStateCssClass { get; set; } = "offline";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsMenuOpen { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string MenuButtonId { get; set; } = "screen-menu-button";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string MenuId { get; set; } = "screen-menu";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IReadOnlyList<AppHeaderMenuItem> MenuItems { get; set; } = [];
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback ToggleMenuRequested { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback LogoutRequested { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class AppHeaderMenuItem
|
||||||
|
{
|
||||||
|
public string Label { get; init; } = string.Empty;
|
||||||
|
public bool IsActive { get; init; }
|
||||||
|
public Func<Task>? OnSelected { get; init; }
|
||||||
|
}
|
||||||
@@ -79,143 +79,45 @@
|
|||||||
@foreach (var group in visibleSkillGroups)
|
@foreach (var group in visibleSkillGroups)
|
||||||
{
|
{
|
||||||
var groupSkills = filteredSkills.Where(skill => skill.SkillGroupId == group.Id).ToList();
|
var groupSkills = filteredSkills.Where(skill => skill.SkillGroupId == group.Id).ToList();
|
||||||
<div class="skill-group-block">
|
<SkillGroupBlock
|
||||||
<div class="skill-group-head">
|
Title="@group.Name"
|
||||||
<strong>@group.Name</strong>
|
SkillGroupId="group.Id"
|
||||||
<div class="skill-chip-actions">
|
Skills="groupSkills"
|
||||||
<button
|
IsMutating="IsMutating"
|
||||||
type="button"
|
CanEditGroup="CanEditCharacter(SelectedCharacter)"
|
||||||
class="chip-button"
|
CanCreateSkill="CanEditCharacter(SelectedCharacter)"
|
||||||
title="Edit skill group"
|
HasSkillFilter="hasSkillFilter"
|
||||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
EmptyMessage="No skills in this group yet."
|
||||||
@onclick="() => OpenEditSkillGroupModal(group)">
|
ShowGroupActions="true"
|
||||||
<span aria-hidden="true" class="emoji">✏️</span>
|
CanEditSkill="CanEditSkill"
|
||||||
<span class="sr-only">Edit @group.Name</span>
|
SkillDefinitionLabel="SkillDefinitionLabel"
|
||||||
</button>
|
AddSkillRequested="OnAddSkillRequestedAsync"
|
||||||
<button
|
EditSkillRequested="OnEditSkillRequestedAsync"
|
||||||
type="button"
|
RollSkillRequested="RollSkillAsync"
|
||||||
class="chip-button"
|
DeleteSkillRequested="DeleteSkillAsync"
|
||||||
title="Delete skill group"
|
EditGroupRequested="OnEditSkillGroupRequestedAsync"
|
||||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
DeleteGroupRequested="DeleteSkillGroupAsync"/>
|
||||||
@onclick="() => DeleteSkillGroupAsync(group.Id)">
|
|
||||||
<span aria-hidden="true" class="emoji">🗑️</span>
|
|
||||||
<span class="sr-only">Delete @group.Name</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@if (!hasSkillFilter && groupSkills.Count == 0)
|
|
||||||
{
|
|
||||||
<p class="empty">No skills in this group yet.</p>
|
|
||||||
}
|
|
||||||
<div class="skill-list">
|
|
||||||
@foreach (var skill in groupSkills)
|
|
||||||
{
|
|
||||||
<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(group.Id)">
|
|
||||||
<span class="skill-create-icon" aria-hidden="true">+</span>
|
|
||||||
<span>Add skill</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
@if (!hasSkillFilter || ungroupedSkills.Count > 0)
|
@if (!hasSkillFilter || ungroupedSkills.Count > 0)
|
||||||
{
|
{
|
||||||
<div class="skill-group-block">
|
<SkillGroupBlock
|
||||||
<div class="skill-group-head">
|
Title="Ungrouped"
|
||||||
<strong>Ungrouped</strong>
|
SkillGroupId="@((Guid?)null)"
|
||||||
</div>
|
Skills="ungroupedSkills"
|
||||||
@if (!hasSkillFilter && ungroupedSkills.Count == 0)
|
IsMutating="IsMutating"
|
||||||
{
|
CanEditGroup="false"
|
||||||
<p class="empty">No ungrouped skills.</p>
|
CanCreateSkill="CanEditCharacter(SelectedCharacter)"
|
||||||
}
|
HasSkillFilter="hasSkillFilter"
|
||||||
<div class="skill-list">
|
EmptyMessage="No ungrouped skills."
|
||||||
@foreach (var skill in ungroupedSkills)
|
ShowGroupActions="false"
|
||||||
{
|
CanEditSkill="CanEditSkill"
|
||||||
<div class="skill-item">
|
SkillDefinitionLabel="SkillDefinitionLabel"
|
||||||
<div class="skill-details">
|
AddSkillRequested="OnAddSkillRequestedAsync"
|
||||||
<strong>@skill.Name</strong>
|
EditSkillRequested="OnEditSkillRequestedAsync"
|
||||||
<span>@SkillDefinitionLabel(skill)</span>
|
RollSkillRequested="RollSkillAsync"
|
||||||
</div>
|
DeleteSkillRequested="DeleteSkillAsync"
|
||||||
<div class="skill-chip-actions">
|
EditGroupRequested="OnEditSkillGroupRequestedAsync"
|
||||||
<button
|
DeleteGroupRequested="DeleteSkillGroupAsync"/>
|
||||||
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>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -72,6 +72,27 @@ public partial class CharacterPanel
|
|||||||
await RollRequested.InvokeAsync(skillId);
|
await RollRequested.InvokeAsync(skillId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task OnAddSkillRequestedAsync(Guid? skillGroupId)
|
||||||
|
{
|
||||||
|
OpenCreateSkillModal(skillGroupId);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task OnEditSkillRequestedAsync(SkillSummary skill)
|
||||||
|
{
|
||||||
|
OpenEditSkillModal(skill);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task OnEditSkillGroupRequestedAsync(Guid skillGroupId)
|
||||||
|
{
|
||||||
|
var skillGroup = SelectedCharacterSkillGroups.FirstOrDefault(group => group.Id == skillGroupId);
|
||||||
|
if (skillGroup is not null)
|
||||||
|
OpenEditSkillGroupModal(skillGroup);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
private void OpenCreateSkillGroupModal()
|
private void OpenCreateSkillGroupModal()
|
||||||
{
|
{
|
||||||
SkillGroupState.Model.Name = string.Empty;
|
SkillGroupState.Model.Name = string.Empty;
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<div class="skill-group-block">
|
||||||
|
<div class="skill-group-head">
|
||||||
|
<strong>@Title</strong>
|
||||||
|
@if (ShowGroupActions && SkillGroupId.HasValue)
|
||||||
|
{
|
||||||
|
<div class="skill-chip-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="chip-button"
|
||||||
|
title="Edit skill group"
|
||||||
|
disabled="@(IsMutating || !CanEditGroup)"
|
||||||
|
@onclick="() => EditGroupRequested.InvokeAsync(SkillGroupId.Value)">
|
||||||
|
<span aria-hidden="true" class="emoji">✏️</span>
|
||||||
|
<span class="sr-only">Edit @Title</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="chip-button"
|
||||||
|
title="Delete skill group"
|
||||||
|
disabled="@(IsMutating || !CanEditGroup)"
|
||||||
|
@onclick="() => DeleteGroupRequested.InvokeAsync(SkillGroupId.Value)">
|
||||||
|
<span aria-hidden="true" class="emoji">🗑️</span>
|
||||||
|
<span class="sr-only">Delete @Title</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (!HasSkillFilter && Skills.Count == 0)
|
||||||
|
{
|
||||||
|
<p class="empty">@EmptyMessage</p>
|
||||||
|
}
|
||||||
|
<div class="skill-list">
|
||||||
|
@foreach (var skill in Skills)
|
||||||
|
{
|
||||||
|
<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="() => EditSkillRequested.InvokeAsync(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="() => RollSkillRequested.InvokeAsync(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="() => DeleteSkillRequested.InvokeAsync(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 || !CanCreateSkill)"
|
||||||
|
@onclick="() => AddSkillRequested.InvokeAsync(SkillGroupId)">
|
||||||
|
<span class="skill-create-icon" aria-hidden="true">+</span>
|
||||||
|
<span>Add skill</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
|
namespace RpgRoller.Components.Pages.HomeControls;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public partial class SkillGroupBlock
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Guid? SkillGroupId { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IReadOnlyList<SkillSummary> Skills { get; set; } = [];
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsMutating { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool CanEditGroup { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool CanCreateSkill { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool HasSkillFilter { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string EmptyMessage { get; set; } = "No skills in this group yet.";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowGroupActions { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<SkillSummary, bool> CanEditSkill { get; set; } = _ => false;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<SkillSummary, string> SkillDefinitionLabel { get; set; } = _ => string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<Guid?> AddSkillRequested { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<SkillSummary> EditSkillRequested { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<Guid> RollSkillRequested { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<Guid> DeleteSkillRequested { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<Guid> EditGroupRequested { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<Guid> DeleteGroupRequested { get; set; }
|
||||||
|
}
|
||||||
@@ -14,59 +14,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="workspace-shell">
|
<div class="workspace-shell">
|
||||||
<header class="workspace-header">
|
<AppHeader
|
||||||
<div class="header-row">
|
User="User"
|
||||||
<h1>RpgRoller</h1>
|
ShowCampaign="true"
|
||||||
@if (User is null)
|
CampaignName="SelectedCampaignName"
|
||||||
{
|
ShowConnectionState="true"
|
||||||
<p class="header-identity"><strong>Loading user...</strong></p>
|
ConnectionStateLabel="ConnectionStateLabel"
|
||||||
}
|
ConnectionStateCssClass="ConnectionStateCssClass"
|
||||||
else
|
IsMenuOpen="IsScreenMenuOpen"
|
||||||
{
|
MenuButtonId="workspace-screen-menu-button"
|
||||||
<p class="header-identity"><strong>@User.DisplayName</strong> <span class="muted">(@User.Username)</span></p>
|
MenuId="workspace-screen-menu"
|
||||||
}
|
MenuItems="HeaderMenuItems"
|
||||||
<p class="header-campaign">Campaign: <strong>@(SelectedCampaignName ?? "No campaign selected")</strong></p>
|
ToggleMenuRequested="ToggleScreenMenu"
|
||||||
<div class="header-connection-cell">
|
LogoutRequested="LogoutAsync"/>
|
||||||
<p class="connection @ConnectionStateCssClass">@ConnectionStateLabel</p>
|
|
||||||
</div>
|
|
||||||
<a href="" class="logout-link" @onclick:preventDefault="true" @onclick="LogoutAsync">Logout</a>
|
|
||||||
<div class="header-menu-wrap">
|
|
||||||
<button
|
|
||||||
id="screen-menu-button"
|
|
||||||
type="button"
|
|
||||||
class="menu-toggle"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded="@IsScreenMenuOpen"
|
|
||||||
aria-controls="screen-menu"
|
|
||||||
@onclick="ToggleScreenMenu">
|
|
||||||
<span aria-hidden="true">☰</span>
|
|
||||||
</button>
|
|
||||||
@if (IsScreenMenuOpen)
|
|
||||||
{
|
|
||||||
<div id="screen-menu" class="screen-menu" role="menu" aria-labelledby="screen-menu-button">
|
|
||||||
<button type="button"
|
|
||||||
class="menu-item @(IsPlayScreen ? "active" : string.Empty)"
|
|
||||||
role="menuitem"
|
|
||||||
@onclick="SwitchToPlayAsync">Play
|
|
||||||
</button>
|
|
||||||
<button type="button"
|
|
||||||
class="menu-item @(IsManagementScreen ? "active" : string.Empty)"
|
|
||||||
role="menuitem"
|
|
||||||
@onclick="SwitchToManagementAsync">Campaign Management
|
|
||||||
</button>
|
|
||||||
@if (IsCurrentUserAdmin)
|
|
||||||
{
|
|
||||||
<button type="button"
|
|
||||||
class="menu-item"
|
|
||||||
role="menuitem"
|
|
||||||
@onclick="OpenAdminAsync">Admin
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
@if (IsPlayScreen)
|
@if (IsPlayScreen)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
using RpgRoller.Components.Pages.HomeControls;
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
using RpgRoller.Domain;
|
using RpgRoller.Domain;
|
||||||
|
|
||||||
@@ -846,7 +847,22 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
|
|
||||||
private bool IsPlayScreen => string.Equals(CurrentScreen, "play", StringComparison.OrdinalIgnoreCase);
|
private bool IsPlayScreen => string.Equals(CurrentScreen, "play", StringComparison.OrdinalIgnoreCase);
|
||||||
private bool IsManagementScreen => !IsPlayScreen;
|
private bool IsManagementScreen => !IsPlayScreen;
|
||||||
private string CurrentScreenLabel => IsPlayScreen ? "Play" : "Campaign Management";
|
private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var items = new List<AppHeaderMenuItem>
|
||||||
|
{
|
||||||
|
new() { Label = "Play", IsActive = IsPlayScreen, OnSelected = SwitchToPlayAsync },
|
||||||
|
new() { Label = "Campaign Management", IsActive = IsManagementScreen, OnSelected = SwitchToManagementAsync }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (IsCurrentUserAdmin)
|
||||||
|
items.Add(new AppHeaderMenuItem { Label = "Admin", IsActive = false, OnSelected = OpenAdminAsync });
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string ConnectionStateLabel => ConnectionState switch
|
private string ConnectionStateLabel => ConnectionState switch
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user