refactor: split workspace routes
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
@page "/admin"
|
||||
@inherits AuthenticatedPageBase
|
||||
<Workspace Route="WorkspaceRoute.Admin" LoggedOut="OnLoggedOutAsync"/>
|
||||
<Workspace Route="WorkspaceRoute.Admin" LoggedOut="OnLoggedOutAsync">
|
||||
<ChildContent Context="workspace">
|
||||
<AdminWorkspaceContent Workspace="workspace"/>
|
||||
</ChildContent>
|
||||
</Workspace>
|
||||
|
||||
68
RpgRoller/Components/Pages/AdminWorkspaceContent.razor
Normal file
68
RpgRoller/Components/Pages/AdminWorkspaceContent.razor
Normal file
@@ -0,0 +1,68 @@
|
||||
@using Microsoft.AspNetCore.Components
|
||||
|
||||
<main class="management-screen">
|
||||
@if (Workspace.State.IsCurrentUserAdmin)
|
||||
{
|
||||
<section class="card">
|
||||
<div class="section-head">
|
||||
<h2>Database</h2>
|
||||
</div>
|
||||
<p class="muted">Download the current SQLite file for backup or offline inspection.</p>
|
||||
<div class="management-actions">
|
||||
<a class="action-link" href="@Workspace.AdminDatabaseDownloadUrl" download>Download SQLite database</a>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
<section class="card">
|
||||
<div class="section-head">
|
||||
<h2>User Management</h2>
|
||||
</div>
|
||||
@if (Workspace.State.IsAdminDataLoading)
|
||||
{
|
||||
<p class="empty">Loading users...</p>
|
||||
}
|
||||
else if (!Workspace.State.IsCurrentUserAdmin)
|
||||
{
|
||||
<p class="empty">Admin role is required to manage users.</p>
|
||||
}
|
||||
else if (Workspace.State.AdminUsers.Count == 0)
|
||||
{
|
||||
<p class="empty">No users found.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="management-list">
|
||||
@foreach (var user in Workspace.State.AdminUsers)
|
||||
{
|
||||
<li>
|
||||
<div>
|
||||
<strong>@user.Username</strong>
|
||||
<p class="muted">@user.DisplayName</p>
|
||||
<p class="muted">Roles: @(user.Roles.Count == 0 ? "none" : string.Join(", ", user.Roles))</p>
|
||||
</div>
|
||||
<div class="skill-chip-actions">
|
||||
<button type="button"
|
||||
class="chip-button"
|
||||
disabled="@(Workspace.State.IsMutating || user.Id == Workspace.State.User?.Id)"
|
||||
@onclick="() => Workspace.Admin.ToggleAdminRoleAsync(user)">
|
||||
<span aria-hidden="true" class="emoji">🛡️</span>
|
||||
<span class="sr-only">Toggle admin role for @user.Username</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="chip-button"
|
||||
disabled="@(Workspace.State.IsMutating || user.Id == Workspace.State.User?.Id)"
|
||||
@onclick="() => Workspace.Admin.DeleteUserAsync(user)">
|
||||
<span aria-hidden="true" class="emoji">🗑️</span>
|
||||
<span class="sr-only">Delete user @user.Username</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
@page "/campaigns"
|
||||
@inherits AuthenticatedPageBase
|
||||
<Workspace Route="WorkspaceRoute.Campaigns" LoggedOut="OnLoggedOutAsync"/>
|
||||
<Workspace Route="WorkspaceRoute.Campaigns" LoggedOut="OnLoggedOutAsync">
|
||||
<ChildContent Context="workspace">
|
||||
<CampaignsWorkspaceContent Workspace="workspace"/>
|
||||
</ChildContent>
|
||||
</Workspace>
|
||||
|
||||
31
RpgRoller/Components/Pages/CampaignsWorkspaceContent.razor
Normal file
31
RpgRoller/Components/Pages/CampaignsWorkspaceContent.razor
Normal file
@@ -0,0 +1,31 @@
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using RpgRoller.Components.Pages.HomeControls
|
||||
|
||||
<CampaignManagementPanel
|
||||
Campaigns="Workspace.State.Campaigns"
|
||||
SelectedCampaignId="Workspace.State.SelectedCampaignId"
|
||||
SelectedCampaign="Workspace.State.SelectedCampaign"
|
||||
Rulesets="Workspace.State.Rulesets"
|
||||
IsMutating="Workspace.State.IsMutating"
|
||||
OwnerLabel="Workspace.State.OwnerLabel"
|
||||
CanEditCharacter="Workspace.Campaigns.CanEditCharacter"
|
||||
CanDeleteCharacter="Workspace.Campaigns.CanDeleteCharacter"
|
||||
CanDeleteCampaign="Workspace.State.CanDeleteSelectedCampaign"
|
||||
CampaignSelectionChanged="OnCampaignSelectionChangedAsync"
|
||||
CampaignCreated="Workspace.Campaigns.OnCampaignCreatedAsync"
|
||||
DeleteCampaignRequested="Workspace.Campaigns.DeleteSelectedCampaignAsync"
|
||||
CreateCharacterRequested="Workspace.Campaigns.OpenCreateCharacterModal"
|
||||
EditCharacterRequested="Workspace.Campaigns.OpenEditCharacterModal"
|
||||
DeleteCharacterRequested="Workspace.Campaigns.DeleteCharacterAsync"/>
|
||||
|
||||
<CharacterManagementModals Workspace="Workspace"/>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
||||
|
||||
private async Task OnCampaignSelectionChangedAsync(ChangeEventArgs args)
|
||||
{
|
||||
await Workspace.Campaigns.OnCampaignSelectionChangedAsync(args);
|
||||
await Workspace.RequestRefreshAsync();
|
||||
}
|
||||
}
|
||||
40
RpgRoller/Components/Pages/CharacterManagementModals.razor
Normal file
40
RpgRoller/Components/Pages/CharacterManagementModals.razor
Normal file
@@ -0,0 +1,40 @@
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using RpgRoller.Components.Pages.HomeControls
|
||||
|
||||
<CharacterFormModal
|
||||
Visible="Workspace.State.ShowCreateCharacterModal"
|
||||
Title="Create Character"
|
||||
SubmitLabel="Create Character"
|
||||
NameInputId="character-create-name"
|
||||
CampaignInputId="character-create-campaign"
|
||||
OwnerUsernameInputId="character-create-owner"
|
||||
InitialModel="Workspace.State.CreateCharacterInitialModel"
|
||||
FormVersion="Workspace.State.CreateCharacterFormVersion"
|
||||
EditingCharacterId="null"
|
||||
CampaignOptions="Workspace.State.CharacterCampaignOptions"
|
||||
IsMutating="Workspace.State.IsMutating"
|
||||
AllowOwnerEdit="false"
|
||||
AvailableUsernames="Workspace.State.KnownUsernames"
|
||||
CharacterSaved="Workspace.Campaigns.OnCharacterCreatedAsync"
|
||||
CancelRequested="Workspace.Campaigns.CloseCharacterModals"/>
|
||||
|
||||
<CharacterFormModal
|
||||
Visible="Workspace.State.ShowEditCharacterModal"
|
||||
Title="Edit Character"
|
||||
SubmitLabel="Save Character"
|
||||
NameInputId="character-edit-name"
|
||||
CampaignInputId="character-edit-campaign"
|
||||
OwnerUsernameInputId="character-edit-owner"
|
||||
InitialModel="Workspace.State.EditCharacterInitialModel"
|
||||
FormVersion="Workspace.State.EditCharacterFormVersion"
|
||||
EditingCharacterId="Workspace.State.EditingCharacterId"
|
||||
CampaignOptions="Workspace.State.CharacterCampaignOptions"
|
||||
IsMutating="Workspace.State.IsMutating"
|
||||
AllowOwnerEdit="Workspace.State.CanEditCharacterOwner"
|
||||
AvailableUsernames="Workspace.State.KnownUsernames"
|
||||
CharacterSaved="Workspace.Campaigns.OnCharacterUpdatedAsync"
|
||||
CancelRequested="Workspace.Campaigns.CloseCharacterModals"/>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
@page "/play"
|
||||
@inherits AuthenticatedPageBase
|
||||
<Workspace Route="WorkspaceRoute.Play" LoggedOut="OnLoggedOutAsync"/>
|
||||
<Workspace Route="WorkspaceRoute.Play" LoggedOut="OnLoggedOutAsync">
|
||||
<ChildContent Context="workspace">
|
||||
<PlayWorkspaceContent Workspace="workspace"/>
|
||||
</ChildContent>
|
||||
</Workspace>
|
||||
|
||||
77
RpgRoller/Components/Pages/PlayWorkspaceContent.razor
Normal file
77
RpgRoller/Components/Pages/PlayWorkspaceContent.razor
Normal file
@@ -0,0 +1,77 @@
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using RpgRoller.Components.Pages.HomeControls
|
||||
|
||||
<main class="play-screen @(Workspace.State.MobilePanel == "log" ? "mobile-log" : "mobile-character")">
|
||||
<CharacterPanel
|
||||
IsCampaignDataLoading="Workspace.State.IsCampaignDataLoading"
|
||||
SelectedCampaign="Workspace.State.PlaySelectedCampaign"
|
||||
SelectedCharacterId="Workspace.State.PlaySelectedCharacterId"
|
||||
SelectedCharacter="Workspace.State.PlaySelectedCharacter"
|
||||
IsMutating="Workspace.State.IsMutating"
|
||||
SelectedCharacterSkills="Workspace.State.PlaySelectedCharacterSkills"
|
||||
SelectedCharacterSkillGroups="Workspace.State.PlaySelectedCharacterSkillGroups"
|
||||
SelectedCampaignRulesetId="@(Workspace.State.PlaySelectedCampaign?.RulesetId ?? string.Empty)"
|
||||
RollVisibility="Workspace.State.RollVisibility"
|
||||
EnableInteractiveControls="Workspace.EnableCharacterControls"
|
||||
RollVisibilityChanged="Workspace.Session.OnRollVisibilityChangedAsync"
|
||||
OwnerLabel="Workspace.State.OwnerLabel"
|
||||
SkillDefinitionLabel="Workspace.State.SkillDefinitionLabel"
|
||||
CanEditCharacter="Workspace.Campaigns.CanEditCharacter"
|
||||
CanEditSkill="Workspace.Play.CanEditSkill"
|
||||
CharacterSelected="Workspace.Play.SelectCharacterAsync"
|
||||
EditCharacterRequested="Workspace.Campaigns.OpenEditCharacterModal"
|
||||
SkillCreated="Workspace.Play.OnSkillCreatedAsync"
|
||||
SkillUpdated="Workspace.Play.OnSkillUpdatedAsync"
|
||||
SkillGroupCreated="Workspace.Play.OnSkillGroupCreatedAsync"
|
||||
SkillGroupUpdated="Workspace.Play.OnSkillGroupUpdatedAsync"
|
||||
SkillDeleted="Workspace.Play.OnSkillDeletedAsync"
|
||||
SkillGroupDeleted="Workspace.Play.OnSkillGroupDeletedAsync"
|
||||
ErrorOccurred="Workspace.Play.OnCharacterPanelErrorAsync"
|
||||
RollRequested="Workspace.Play.RollSkillAsync"/>
|
||||
|
||||
<CampaignLogPanel
|
||||
IsCampaignDataLoading="Workspace.State.IsCampaignDataLoading"
|
||||
CampaignLog="Workspace.State.PlayVisibleCampaignLog"
|
||||
ExpandedRollId="Workspace.State.ExpandedCampaignLogRollId"
|
||||
FreshRollId="Workspace.State.FreshCampaignLogRollId"
|
||||
SelectedCharacterId="Workspace.State.PlaySelectedCharacterId"
|
||||
SelectedCharacterName="@(Workspace.State.PlaySelectedCharacter?.Name)"
|
||||
SelectedCampaignRulesetId="@(Workspace.State.PlaySelectedCampaign?.RulesetId ?? string.Empty)"
|
||||
RollVisibility="Workspace.State.RollVisibility"
|
||||
EnableCustomRollComposer="Workspace.EnableCustomRollComposer"
|
||||
IsMutating="Workspace.State.IsMutating"
|
||||
ToggleRollDetailRequested="Workspace.Play.ToggleRollDetailAsync"
|
||||
ResolveRollDetail="Workspace.Play.ResolveRollDetail"
|
||||
IsRollDetailLoading="Workspace.Play.IsRollDetailLoading"
|
||||
GetRollDetailError="Workspace.Play.GetRollDetailError"
|
||||
CustomRollCreated="Workspace.Play.OnCustomRollCreatedAsync"
|
||||
ErrorOccurred="Workspace.Play.OnCampaignLogPanelErrorAsync"/>
|
||||
</main>
|
||||
<nav class="mobile-bottom-nav" aria-label="Play panel selector">
|
||||
<button type="button" class="switch @(Workspace.State.MobilePanel == "character" ? "active" : string.Empty)"
|
||||
@onclick='() => Workspace.Scope.SetMobilePanelAsync("character")'>
|
||||
Character
|
||||
</button>
|
||||
<button type="button" class="switch @(Workspace.State.MobilePanel == "log" ? "active" : string.Empty)"
|
||||
@onclick='() => Workspace.Scope.SetMobilePanelAsync("log")'>
|
||||
Log
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<CharacterManagementModals Workspace="Workspace"/>
|
||||
|
||||
<RolemasterSkillRollModal
|
||||
Visible="Workspace.State.ShowRolemasterSkillRollModal"
|
||||
SkillName="@(Workspace.State.PendingRolemasterSkillRoll?.Name ?? string.Empty)"
|
||||
Expression="@(Workspace.State.PendingRolemasterSkillRoll?.DiceRollDefinition ?? string.Empty)"
|
||||
ModifierText="@Workspace.State.PendingRolemasterSituationalModifier"
|
||||
ModifierTextChanged="@(text => Workspace.State.PendingRolemasterSituationalModifier = text)"
|
||||
ErrorMessage="@Workspace.State.PendingRolemasterSkillRollError"
|
||||
IsMutating="Workspace.State.IsMutating"
|
||||
IsSubmitting="Workspace.State.IsSubmittingRolemasterSkillRoll"
|
||||
ConfirmRequested="Workspace.Play.SubmitRolemasterSkillRollAsync"
|
||||
CancelRequested="Workspace.Play.CancelRolemasterSkillRollAsync"/>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
||||
}
|
||||
@@ -27,149 +27,9 @@
|
||||
MenuItems="HeaderMenuItems"
|
||||
ToggleMenuRequested="ToggleScreenMenu"
|
||||
LogoutRequested="Session.LogoutAsync"/>
|
||||
|
||||
@if (IsPlayRoute)
|
||||
@if (ChildContent is not null)
|
||||
{
|
||||
<main class="play-screen @(State.MobilePanel == "log" ? "mobile-log" : "mobile-character")">
|
||||
<CharacterPanel
|
||||
IsCampaignDataLoading="State.IsCampaignDataLoading"
|
||||
SelectedCampaign="State.PlaySelectedCampaign"
|
||||
SelectedCharacterId="State.PlaySelectedCharacterId"
|
||||
SelectedCharacter="State.PlaySelectedCharacter"
|
||||
IsMutating="State.IsMutating"
|
||||
SelectedCharacterSkills="State.PlaySelectedCharacterSkills"
|
||||
SelectedCharacterSkillGroups="State.PlaySelectedCharacterSkillGroups"
|
||||
SelectedCampaignRulesetId="@(State.PlaySelectedCampaign?.RulesetId ?? string.Empty)"
|
||||
RollVisibility="State.RollVisibility"
|
||||
EnableInteractiveControls="EnableCharacterControls"
|
||||
RollVisibilityChanged="Session.OnRollVisibilityChangedAsync"
|
||||
OwnerLabel="State.OwnerLabel"
|
||||
SkillDefinitionLabel="State.SkillDefinitionLabel"
|
||||
CanEditCharacter="Campaigns.CanEditCharacter"
|
||||
CanEditSkill="Play.CanEditSkill"
|
||||
CharacterSelected="Play.SelectCharacterAsync"
|
||||
EditCharacterRequested="Campaigns.OpenEditCharacterModal"
|
||||
SkillCreated="Play.OnSkillCreatedAsync"
|
||||
SkillUpdated="Play.OnSkillUpdatedAsync"
|
||||
SkillGroupCreated="Play.OnSkillGroupCreatedAsync"
|
||||
SkillGroupUpdated="Play.OnSkillGroupUpdatedAsync"
|
||||
SkillDeleted="Play.OnSkillDeletedAsync"
|
||||
SkillGroupDeleted="Play.OnSkillGroupDeletedAsync"
|
||||
ErrorOccurred="Play.OnCharacterPanelErrorAsync"
|
||||
RollRequested="Play.RollSkillAsync"/>
|
||||
|
||||
<CampaignLogPanel
|
||||
IsCampaignDataLoading="State.IsCampaignDataLoading"
|
||||
CampaignLog="State.PlayVisibleCampaignLog"
|
||||
ExpandedRollId="State.ExpandedCampaignLogRollId"
|
||||
FreshRollId="State.FreshCampaignLogRollId"
|
||||
SelectedCharacterId="State.PlaySelectedCharacterId"
|
||||
SelectedCharacterName="@(State.PlaySelectedCharacter?.Name)"
|
||||
SelectedCampaignRulesetId="@(State.PlaySelectedCampaign?.RulesetId ?? string.Empty)"
|
||||
RollVisibility="State.RollVisibility"
|
||||
EnableCustomRollComposer="EnableCustomRollComposer"
|
||||
IsMutating="State.IsMutating"
|
||||
ToggleRollDetailRequested="Play.ToggleRollDetailAsync"
|
||||
ResolveRollDetail="Play.ResolveRollDetail"
|
||||
IsRollDetailLoading="Play.IsRollDetailLoading"
|
||||
GetRollDetailError="Play.GetRollDetailError"
|
||||
CustomRollCreated="Play.OnCustomRollCreatedAsync"
|
||||
ErrorOccurred="Play.OnCampaignLogPanelErrorAsync"/>
|
||||
</main>
|
||||
<nav class="mobile-bottom-nav" aria-label="Play panel selector">
|
||||
<button type="button" class="switch @(State.MobilePanel == "character" ? "active" : string.Empty)"
|
||||
@onclick='() => Scope.SetMobilePanelAsync("character")'>
|
||||
Character
|
||||
</button>
|
||||
<button type="button" class="switch @(State.MobilePanel == "log" ? "active" : string.Empty)"
|
||||
@onclick='() => Scope.SetMobilePanelAsync("log")'>
|
||||
Log
|
||||
</button>
|
||||
</nav>
|
||||
}
|
||||
else if (IsCampaignsRoute)
|
||||
{
|
||||
<CampaignManagementPanel
|
||||
Campaigns="State.Campaigns"
|
||||
SelectedCampaignId="State.SelectedCampaignId"
|
||||
SelectedCampaign="State.SelectedCampaign"
|
||||
Rulesets="State.Rulesets"
|
||||
IsMutating="State.IsMutating"
|
||||
OwnerLabel="State.OwnerLabel"
|
||||
CanEditCharacter="Campaigns.CanEditCharacter"
|
||||
CanDeleteCharacter="Campaigns.CanDeleteCharacter"
|
||||
CanDeleteCampaign="State.CanDeleteSelectedCampaign"
|
||||
CampaignSelectionChanged="Campaigns.OnCampaignSelectionChangedAsync"
|
||||
CampaignCreated="Campaigns.OnCampaignCreatedAsync"
|
||||
DeleteCampaignRequested="Campaigns.DeleteSelectedCampaignAsync"
|
||||
CreateCharacterRequested="Campaigns.OpenCreateCharacterModal"
|
||||
EditCharacterRequested="Campaigns.OpenEditCharacterModal"
|
||||
DeleteCharacterRequested="Campaigns.DeleteCharacterAsync"/>
|
||||
}
|
||||
else if (IsAdminRoute)
|
||||
{
|
||||
<main class="management-screen">
|
||||
@if (State.IsCurrentUserAdmin)
|
||||
{
|
||||
<section class="card">
|
||||
<div class="section-head">
|
||||
<h2>Database</h2>
|
||||
</div>
|
||||
<p class="muted">Download the current SQLite file for backup or offline inspection.</p>
|
||||
<div class="management-actions">
|
||||
<a class="action-link" href="@AdminDatabaseDownloadUrl" download>Download SQLite database</a>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
<section class="card">
|
||||
<div class="section-head">
|
||||
<h2>User Management</h2>
|
||||
</div>
|
||||
@if (State.IsAdminDataLoading)
|
||||
{
|
||||
<p class="empty">Loading users...</p>
|
||||
}
|
||||
else if (!State.IsCurrentUserAdmin)
|
||||
{
|
||||
<p class="empty">Admin role is required to manage users.</p>
|
||||
}
|
||||
else if (State.AdminUsers.Count == 0)
|
||||
{
|
||||
<p class="empty">No users found.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="management-list">
|
||||
@foreach (var user in State.AdminUsers)
|
||||
{
|
||||
<li>
|
||||
<div>
|
||||
<strong>@user.Username</strong>
|
||||
<p class="muted">@user.DisplayName</p>
|
||||
<p class="muted">Roles: @(user.Roles.Count == 0 ? "none" : string.Join(", ", user.Roles))</p>
|
||||
</div>
|
||||
<div class="skill-chip-actions">
|
||||
<button type="button"
|
||||
class="chip-button"
|
||||
disabled="@(State.IsMutating || user.Id == State.User?.Id)"
|
||||
@onclick="() => Admin.ToggleAdminRoleAsync(user)">
|
||||
<span aria-hidden="true" class="emoji">🛡️</span>
|
||||
<span class="sr-only">Toggle admin role for @user.Username</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="chip-button"
|
||||
disabled="@(State.IsMutating || user.Id == State.User?.Id)"
|
||||
@onclick="() => Admin.DeleteUserAsync(user)">
|
||||
<span aria-hidden="true" class="emoji">🗑️</span>
|
||||
<span class="sr-only">Delete user @user.Username</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</section>
|
||||
</main>
|
||||
@ChildContent(PageContext)
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -185,49 +45,3 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<CharacterFormModal
|
||||
Visible="State.ShowCreateCharacterModal"
|
||||
Title="Create Character"
|
||||
SubmitLabel="Create Character"
|
||||
NameInputId="character-create-name"
|
||||
CampaignInputId="character-create-campaign"
|
||||
OwnerUsernameInputId="character-create-owner"
|
||||
InitialModel="State.CreateCharacterInitialModel"
|
||||
FormVersion="State.CreateCharacterFormVersion"
|
||||
EditingCharacterId="null"
|
||||
CampaignOptions="State.CharacterCampaignOptions"
|
||||
IsMutating="State.IsMutating"
|
||||
AllowOwnerEdit="false"
|
||||
AvailableUsernames="State.KnownUsernames"
|
||||
CharacterSaved="Campaigns.OnCharacterCreatedAsync"
|
||||
CancelRequested="Campaigns.CloseCharacterModals"/>
|
||||
|
||||
<CharacterFormModal
|
||||
Visible="State.ShowEditCharacterModal"
|
||||
Title="Edit Character"
|
||||
SubmitLabel="Save Character"
|
||||
NameInputId="character-edit-name"
|
||||
CampaignInputId="character-edit-campaign"
|
||||
OwnerUsernameInputId="character-edit-owner"
|
||||
InitialModel="State.EditCharacterInitialModel"
|
||||
FormVersion="State.EditCharacterFormVersion"
|
||||
EditingCharacterId="State.EditingCharacterId"
|
||||
CampaignOptions="State.CharacterCampaignOptions"
|
||||
IsMutating="State.IsMutating"
|
||||
AllowOwnerEdit="State.CanEditCharacterOwner"
|
||||
AvailableUsernames="State.KnownUsernames"
|
||||
CharacterSaved="Campaigns.OnCharacterUpdatedAsync"
|
||||
CancelRequested="Campaigns.CloseCharacterModals"/>
|
||||
|
||||
<RolemasterSkillRollModal
|
||||
Visible="State.ShowRolemasterSkillRollModal"
|
||||
SkillName="@(State.PendingRolemasterSkillRoll?.Name ?? string.Empty)"
|
||||
Expression="@(State.PendingRolemasterSkillRoll?.DiceRollDefinition ?? string.Empty)"
|
||||
ModifierText="@State.PendingRolemasterSituationalModifier"
|
||||
ModifierTextChanged="@(text => State.PendingRolemasterSituationalModifier = text)"
|
||||
ErrorMessage="@State.PendingRolemasterSkillRollError"
|
||||
IsMutating="State.IsMutating"
|
||||
IsSubmitting="State.IsSubmittingRolemasterSkillRoll"
|
||||
ConfirmRequested="Play.SubmitRolemasterSkillRollAsync"
|
||||
CancelRequested="Play.CancelRolemasterSkillRollAsync"/>
|
||||
|
||||
@@ -121,6 +121,11 @@ public partial class Workspace : IAsyncDisposable
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task RequestRefreshAsync()
|
||||
{
|
||||
return InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private static bool IsStaticRenderInteropException(InvalidOperationException exception)
|
||||
{
|
||||
return exception.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase);
|
||||
@@ -136,6 +141,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
|
||||
[Parameter] public EventCallback<string?> LoggedOut { get; set; }
|
||||
[Parameter] public WorkspaceRoute Route { get; set; } = WorkspaceRoute.Play;
|
||||
[Parameter] public RenderFragment<WorkspacePageContext>? ChildContent { get; set; }
|
||||
|
||||
private WorkspaceState State { get; } = new();
|
||||
private bool HasSessionInitialized { get; set; }
|
||||
@@ -146,6 +152,10 @@ public partial class Workspace : IAsyncDisposable
|
||||
private bool IsAdminRoute => Route == WorkspaceRoute.Admin;
|
||||
private string AppCssClass => IsPlayRoute ? "rr-app app-play" : "rr-app";
|
||||
|
||||
private WorkspacePageContext PageContext => new(State, Play, Campaigns, Admin, Scope, Session,
|
||||
RequestRefreshAsync, EnableCharacterControls, EnableCustomRollComposer, AdminDatabaseDownloadUrl,
|
||||
HeaderMenuItems, IsPlayRoute, IsCampaignsRoute, IsAdminRoute);
|
||||
|
||||
private WorkspaceCampaignScopeCoordinator Scope => m_Scope ??= new(State, Feedback, JS, WorkspaceQuery,
|
||||
() => IsPlayRoute, Play.EnsureSelectedCharacterActiveAsync, Play.RefreshSelectedCharacterSheetAsync,
|
||||
Play.RefreshCampaignLogAsync, Play.ResetCampaignLogDetailState, Play.ResetCampaignStateTracking,
|
||||
|
||||
35
RpgRoller/Components/Pages/WorkspacePageContext.cs
Normal file
35
RpgRoller/Components/Pages/WorkspacePageContext.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using RpgRoller.Components.Pages.HomeControls;
|
||||
|
||||
namespace RpgRoller.Components.Pages;
|
||||
|
||||
public sealed class WorkspacePageContext(
|
||||
WorkspaceState state,
|
||||
WorkspacePlayCoordinator play,
|
||||
WorkspaceCampaignCoordinator campaigns,
|
||||
WorkspaceAdminCoordinator admin,
|
||||
WorkspaceCampaignScopeCoordinator scope,
|
||||
WorkspaceSessionCoordinator session,
|
||||
Func<Task> requestRefreshAsync,
|
||||
bool enableCharacterControls,
|
||||
bool enableCustomRollComposer,
|
||||
string adminDatabaseDownloadUrl,
|
||||
IReadOnlyList<AppHeaderMenuItem> headerMenuItems,
|
||||
bool isPlayRoute,
|
||||
bool isCampaignsRoute,
|
||||
bool isAdminRoute)
|
||||
{
|
||||
public WorkspaceState State { get; } = state;
|
||||
public WorkspacePlayCoordinator Play { get; } = play;
|
||||
public WorkspaceCampaignCoordinator Campaigns { get; } = campaigns;
|
||||
public WorkspaceAdminCoordinator Admin { get; } = admin;
|
||||
public WorkspaceCampaignScopeCoordinator Scope { get; } = scope;
|
||||
public WorkspaceSessionCoordinator Session { get; } = session;
|
||||
public Func<Task> RequestRefreshAsync { get; } = requestRefreshAsync;
|
||||
public bool EnableCharacterControls { get; } = enableCharacterControls;
|
||||
public bool EnableCustomRollComposer { get; } = enableCustomRollComposer;
|
||||
public string AdminDatabaseDownloadUrl { get; } = adminDatabaseDownloadUrl;
|
||||
public IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems { get; } = headerMenuItems;
|
||||
public bool IsPlayRoute { get; } = isPlayRoute;
|
||||
public bool IsCampaignsRoute { get; } = isCampaignsRoute;
|
||||
public bool IsAdminRoute { get; } = isAdminRoute;
|
||||
}
|
||||
4
TASKS.md
4
TASKS.md
@@ -20,7 +20,7 @@ The change is complete when a human can run the app, open `/`, observe the corre
|
||||
- [x] (2026-05-04 19:26Z) Replaced the checked-in Playwright smoke coverage with a geckodriver+Selenium smoke runner, including a Firefox DOM-wrap addon for extension-like startup mutations, and updated repo scripts/docs to the new browser verification path.
|
||||
- [x] (2026-05-04) Introduced real authenticated routes for `/play`, `/campaigns`, and `/admin` while preserving the shared `Workspace` behavior behind those routes.
|
||||
- [x] (2026-05-04) Removed `screen` as a `sessionStorage` routing mechanism and replaced menu actions with URL navigation.
|
||||
- [ ] Split the large `Workspace` render tree so play, campaign management, and admin each own a smaller subtree.
|
||||
- [x] (2026-05-04 21:42Z) Split the large `Workspace` render tree into a shared shell plus route-owned play, campaign-management, and admin content components, and kept the Selenium route and DOM-wrap coverage green after the split.
|
||||
- [ ] Reduce `OnAfterRenderAsync` to the smallest practical scope and keep staged startup out of the authenticated shell root.
|
||||
- [x] (2026-05-04) Updated host tests, Selenium smoke tests, and docs so the real-route model is the documented and verified Milestone 2 behavior.
|
||||
|
||||
@@ -80,6 +80,8 @@ After the Selenium migration iteration, the repository’s browser smoke coverag
|
||||
|
||||
After Milestone 2, the authenticated shell now has first-class `/play`, `/campaigns`, and `/admin` routes, and the menu navigates with URLs instead of `sessionStorage` screen names. The remaining risk is now narrower and more structural: `Workspace.razor` still owns mutually exclusive authenticated branches, and the root `OnAfterRenderAsync` path still stages page-specific startup work that should move into route-owned components in Milestones 3 and 4.
|
||||
|
||||
After Milestone 3, `Workspace.razor` is now a shell that owns shared chrome, health state, and toast feedback, while the play, campaign-management, and admin DOM each live in route-owned components supplied by `/play`, `/campaigns`, and `/admin`. The route split preserved the host tests and full Selenium smoke coverage, including the DOM-wrap regression case, but the final startup path is still staged through `Workspace.razor.cs` and remains the next target for Milestone 4.
|
||||
|
||||
This section must be updated after each major milestone. When the implementation is complete, summarize which parts of the old workspace architecture were fully removed, which compatibility constraints remain, and whether the final startup path still depends on any multi-batch structural rendering.
|
||||
|
||||
## Context and Orientation
|
||||
|
||||
Reference in New Issue
Block a user