Move skill management ownership into CharacterPanel
This commit is contained in:
@@ -9,6 +9,7 @@ Tracking against `UX.md` tasks and decisions.
|
||||
- Legacy TypeScript frontend/runtime artifacts: removed
|
||||
- Home page orchestration split by concern (`Home.*.cs` partials + `HomeControls/*`) to reduce merge churn and keep auth/campaign/character/skill flows isolated.
|
||||
- Concern controls now own their local form state and mutation workflows; `Home` handles shared cross-control state refresh.
|
||||
- Skill create/edit flow is now owned by `CharacterPanel` (where characters and their skills are presented together).
|
||||
|
||||
## UX Checklist
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ Frontend:
|
||||
- `RpgRoller/Components/Pages/Home.Models.cs`: reusable `FormState<TModel>` + page form models
|
||||
- `RpgRoller/Components/Pages/HomeControls/`: auth, campaign management, play-screen, and modal controls extracted from `Home.razor`
|
||||
- Form ownership model: controls own transient form/error state and execute their concern-specific API mutations directly
|
||||
- Skill create/edit workflow ownership: `CharacterPanel` (characters own skills in UI and behavior)
|
||||
- `RpgRoller/Components/RpgRollerApiClient.cs`: shared browser API client used by `Home` and leaf controls
|
||||
- `RpgRoller/wwwroot/js/rpgroller-api.js`: browser-side API + SSE + session storage interop for Blazor
|
||||
- `RpgRoller/wwwroot/styles.css`: responsive UX styling and theme tokens
|
||||
|
||||
@@ -153,16 +153,10 @@ public partial class Home
|
||||
LastRoll = null;
|
||||
ShowCreateCharacterModal = false;
|
||||
ShowEditCharacterModal = false;
|
||||
ShowCreateSkillModal = false;
|
||||
ShowEditSkillModal = false;
|
||||
CreateCharacterInitialModel = new();
|
||||
EditCharacterInitialModel = new();
|
||||
CreateSkillInitialModel = new();
|
||||
EditSkillInitialModel = new();
|
||||
CreateCharacterFormVersion = 0;
|
||||
EditCharacterFormVersion = 0;
|
||||
CreateSkillFormVersion = 0;
|
||||
EditSkillFormVersion = 0;
|
||||
}
|
||||
|
||||
private void SetStatus(string message, bool isError)
|
||||
|
||||
@@ -4,49 +4,8 @@ namespace RpgRoller.Components.Pages;
|
||||
|
||||
public partial class Home
|
||||
{
|
||||
private void OpenCreateSkillModal()
|
||||
{
|
||||
CreateSkillInitialModel = new SkillFormModel
|
||||
{
|
||||
Name = string.Empty,
|
||||
DiceRollDefinition = string.Empty,
|
||||
WildDice = IsSelectedCampaignD6 ? 1 : 0,
|
||||
AllowFumble = IsSelectedCampaignD6
|
||||
};
|
||||
CreateSkillFormVersion++;
|
||||
ShowCreateSkillModal = true;
|
||||
}
|
||||
|
||||
private void OpenEditSkillModal()
|
||||
{
|
||||
if (SelectedSkill is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EditingSkillId = SelectedSkill.Id;
|
||||
|
||||
EditSkillInitialModel = new SkillFormModel
|
||||
{
|
||||
Name = SelectedSkill.Name,
|
||||
DiceRollDefinition = SelectedSkill.DiceRollDefinition,
|
||||
WildDice = SelectedSkill.WildDice,
|
||||
AllowFumble = SelectedSkill.AllowFumble
|
||||
};
|
||||
EditSkillFormVersion++;
|
||||
ShowEditSkillModal = true;
|
||||
}
|
||||
|
||||
private void CloseSkillModals()
|
||||
{
|
||||
ShowCreateSkillModal = false;
|
||||
ShowEditSkillModal = false;
|
||||
EditingSkillId = null;
|
||||
}
|
||||
|
||||
private async Task OnSkillCreatedAsync(Guid _)
|
||||
{
|
||||
CloseSkillModals();
|
||||
await RefreshCampaignScopeAsync();
|
||||
SetStatus("Skill created.", false);
|
||||
}
|
||||
@@ -54,7 +13,6 @@ public partial class Home
|
||||
private async Task OnSkillUpdatedAsync(Guid skillId)
|
||||
{
|
||||
SelectedSkillId = skillId;
|
||||
CloseSkillModals();
|
||||
await RefreshCampaignScopeAsync();
|
||||
SetStatus("Skill updated.", false);
|
||||
}
|
||||
|
||||
@@ -41,18 +41,11 @@ public partial class Home
|
||||
|
||||
private bool ShowCreateCharacterModal { get; set; }
|
||||
private bool ShowEditCharacterModal { get; set; }
|
||||
private bool ShowCreateSkillModal { get; set; }
|
||||
private bool ShowEditSkillModal { get; set; }
|
||||
private Guid? EditingCharacterId { get; set; }
|
||||
private Guid? EditingSkillId { get; set; }
|
||||
private CharacterFormModel CreateCharacterInitialModel { get; set; } = new();
|
||||
private CharacterFormModel EditCharacterInitialModel { get; set; } = new();
|
||||
private SkillFormModel CreateSkillInitialModel { get; set; } = new();
|
||||
private SkillFormModel EditSkillInitialModel { get; set; } = new();
|
||||
private int CreateCharacterFormVersion { get; set; }
|
||||
private int EditCharacterFormVersion { get; set; }
|
||||
private int CreateSkillFormVersion { get; set; }
|
||||
private int EditSkillFormVersion { get; set; }
|
||||
private bool StateRefreshInProgress { get; set; }
|
||||
private bool HasInteractiveRenderStarted { get; set; }
|
||||
private DotNetObjectReference<Home>? DotNetRef { get; set; }
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
SelectedCharacterSkills="SelectedCharacterSkills"
|
||||
SelectedSkillId="SelectedSkillId"
|
||||
SelectedSkill="SelectedSkill"
|
||||
IsD6="IsSelectedCampaignD6"
|
||||
RollVisibility="RollVisibility"
|
||||
RollVisibilityChanged="OnRollVisibilityChanged"
|
||||
LastRoll="LastRoll"
|
||||
@@ -85,8 +86,8 @@
|
||||
CharacterSelected="SelectCharacterAsync"
|
||||
SkillSelected="SelectSkill"
|
||||
EditCharacterRequested="OpenEditCharacterModal"
|
||||
CreateSkillRequested="OpenCreateSkillModal"
|
||||
EditSkillRequested="OpenEditSkillModal"
|
||||
SkillCreated="OnSkillCreatedAsync"
|
||||
SkillUpdated="OnSkillUpdatedAsync"
|
||||
RollRequested="RollSelectedSkillAsync" />
|
||||
|
||||
<CampaignLogPanel
|
||||
@@ -153,37 +154,3 @@
|
||||
IsMutating="IsMutating"
|
||||
CharacterSaved="OnCharacterUpdatedAsync"
|
||||
CancelRequested="CloseCharacterModals" />
|
||||
|
||||
<SkillFormModal
|
||||
Visible="ShowCreateSkillModal"
|
||||
IsD6="IsSelectedCampaignD6"
|
||||
Title="Create Skill"
|
||||
SubmitLabel="Create Skill"
|
||||
NameInputId="skill-create-name"
|
||||
ExpressionInputId="skill-create-expression"
|
||||
WildDiceInputId="skill-create-wild-dice"
|
||||
AllowFumbleInputId="skill-create-allow-fumble"
|
||||
InitialModel="CreateSkillInitialModel"
|
||||
FormVersion="CreateSkillFormVersion"
|
||||
SelectedCharacterId="SelectedCharacterId"
|
||||
EditingSkillId="null"
|
||||
IsMutating="IsMutating"
|
||||
SkillSaved="OnSkillCreatedAsync"
|
||||
CancelRequested="CloseSkillModals" />
|
||||
|
||||
<SkillFormModal
|
||||
Visible="ShowEditSkillModal"
|
||||
IsD6="IsSelectedCampaignD6"
|
||||
Title="Edit Skill"
|
||||
SubmitLabel="Save Skill"
|
||||
NameInputId="skill-edit-name"
|
||||
ExpressionInputId="skill-edit-expression"
|
||||
WildDiceInputId="skill-edit-wild-dice"
|
||||
AllowFumbleInputId="skill-edit-allow-fumble"
|
||||
InitialModel="EditSkillInitialModel"
|
||||
FormVersion="EditSkillFormVersion"
|
||||
SelectedCharacterId="SelectedCharacterId"
|
||||
EditingSkillId="EditingSkillId"
|
||||
IsMutating="IsMutating"
|
||||
SkillSaved="OnSkillUpdatedAsync"
|
||||
CancelRequested="CloseSkillModals" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@using System.Diagnostics.CodeAnalysis
|
||||
@using RpgRoller.Components.Pages
|
||||
@using RpgRoller.Contracts
|
||||
@attribute [ExcludeFromCodeCoverage]
|
||||
|
||||
@@ -43,8 +44,8 @@
|
||||
<div class="section-head">
|
||||
<h3>Skills</h3>
|
||||
<div class="inline-actions">
|
||||
<button type="button" disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))" @onclick="CreateSkillRequested">Create Skill</button>
|
||||
<button type="button" disabled="@(IsMutating || SelectedSkill is null || !CanEditSkill(SelectedSkill))" @onclick="EditSkillRequested">Edit Skill</button>
|
||||
<button type="button" disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))" @onclick="OpenCreateSkillModal">Create Skill</button>
|
||||
<button type="button" disabled="@(IsMutating || SelectedSkill is null || !CanEditSkill(SelectedSkill))" @onclick="OpenEditSkillModal">Edit Skill</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (SelectedCharacterSkills.Count == 0)
|
||||
@@ -90,7 +91,49 @@
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<SkillFormModal
|
||||
Visible="ShowCreateSkillModal"
|
||||
IsD6="IsD6"
|
||||
Title="Create Skill"
|
||||
SubmitLabel="Create Skill"
|
||||
NameInputId="skill-create-name"
|
||||
ExpressionInputId="skill-create-expression"
|
||||
WildDiceInputId="skill-create-wild-dice"
|
||||
AllowFumbleInputId="skill-create-allow-fumble"
|
||||
InitialModel="CreateSkillInitialModel"
|
||||
FormVersion="CreateSkillFormVersion"
|
||||
SelectedCharacterId="SelectedCharacterId"
|
||||
EditingSkillId="null"
|
||||
IsMutating="IsMutating"
|
||||
SkillSaved="OnSkillCreatedAsync"
|
||||
CancelRequested="CloseSkillModals" />
|
||||
|
||||
<SkillFormModal
|
||||
Visible="ShowEditSkillModal"
|
||||
IsD6="IsD6"
|
||||
Title="Edit Skill"
|
||||
SubmitLabel="Save Skill"
|
||||
NameInputId="skill-edit-name"
|
||||
ExpressionInputId="skill-edit-expression"
|
||||
WildDiceInputId="skill-edit-wild-dice"
|
||||
AllowFumbleInputId="skill-edit-allow-fumble"
|
||||
InitialModel="EditSkillInitialModel"
|
||||
FormVersion="EditSkillFormVersion"
|
||||
SelectedCharacterId="SelectedCharacterId"
|
||||
EditingSkillId="EditingSkillId"
|
||||
IsMutating="IsMutating"
|
||||
SkillSaved="OnSkillUpdatedAsync"
|
||||
CancelRequested="CloseSkillModals" />
|
||||
|
||||
@code {
|
||||
private bool ShowCreateSkillModal { get; set; }
|
||||
private bool ShowEditSkillModal { get; set; }
|
||||
private Guid? EditingSkillId { get; set; }
|
||||
private SkillFormModel CreateSkillInitialModel { get; set; } = new();
|
||||
private SkillFormModel EditSkillInitialModel { get; set; } = new();
|
||||
private int CreateSkillFormVersion { get; set; }
|
||||
private int EditSkillFormVersion { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool IsCampaignDataLoading { get; set; }
|
||||
|
||||
@@ -115,6 +158,9 @@
|
||||
[Parameter]
|
||||
public SkillSummary? SelectedSkill { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool IsD6 { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string RollVisibility { get; set; } = "public";
|
||||
|
||||
@@ -149,14 +195,67 @@
|
||||
public EventCallback<CharacterSummary> EditCharacterRequested { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback CreateSkillRequested { get; set; }
|
||||
public EventCallback<Guid> SkillCreated { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback EditSkillRequested { get; set; }
|
||||
public EventCallback<Guid> SkillUpdated { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback RollRequested { get; set; }
|
||||
|
||||
private void OpenCreateSkillModal()
|
||||
{
|
||||
CreateSkillInitialModel = new SkillFormModel
|
||||
{
|
||||
Name = string.Empty,
|
||||
DiceRollDefinition = string.Empty,
|
||||
WildDice = IsD6 ? 1 : 0,
|
||||
AllowFumble = IsD6
|
||||
};
|
||||
|
||||
CreateSkillFormVersion++;
|
||||
ShowCreateSkillModal = true;
|
||||
}
|
||||
|
||||
private void OpenEditSkillModal()
|
||||
{
|
||||
if (SelectedSkill is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EditingSkillId = SelectedSkill.Id;
|
||||
EditSkillInitialModel = new SkillFormModel
|
||||
{
|
||||
Name = SelectedSkill.Name,
|
||||
DiceRollDefinition = SelectedSkill.DiceRollDefinition,
|
||||
WildDice = SelectedSkill.WildDice,
|
||||
AllowFumble = SelectedSkill.AllowFumble
|
||||
};
|
||||
|
||||
EditSkillFormVersion++;
|
||||
ShowEditSkillModal = true;
|
||||
}
|
||||
|
||||
private void CloseSkillModals()
|
||||
{
|
||||
ShowCreateSkillModal = false;
|
||||
ShowEditSkillModal = false;
|
||||
EditingSkillId = null;
|
||||
}
|
||||
|
||||
private async Task OnSkillCreatedAsync(Guid skillId)
|
||||
{
|
||||
CloseSkillModals();
|
||||
await SkillCreated.InvokeAsync(skillId);
|
||||
}
|
||||
|
||||
private async Task OnSkillUpdatedAsync(Guid skillId)
|
||||
{
|
||||
CloseSkillModals();
|
||||
await SkillUpdated.InvokeAsync(skillId);
|
||||
}
|
||||
|
||||
private async Task OnRollVisibilityChangedAsync(ChangeEventArgs args)
|
||||
{
|
||||
var selectedVisibility = args.Value?.ToString() ?? "public";
|
||||
|
||||
1
TECH.md
1
TECH.md
@@ -95,6 +95,7 @@ This pattern is a strong baseline for low to medium scale and should be the defa
|
||||
- Home page logic split by concern with partials (`Home.State/Auth/Campaign/Character/Skill/Lifecycle/Realtime/Api/Presentation/Validation.cs`) to keep churn localized.
|
||||
- Form UX state uses reusable `FormState<TModel>` containers in leaf controls (`HomeControls/*`) rather than parallel form/error/message property sets in `Home`.
|
||||
- Concern controls execute their own auth/campaign/character/skill mutation workflows and notify `Home` only for shared-state refresh/orchestration.
|
||||
- Skill management workflows are owned by `CharacterPanel` to keep character-skill behavior cohesive.
|
||||
- Shared browser API interop is centralized in `RpgRollerApiClient` and reused by `Home` plus concern controls.
|
||||
- Browser API calls and SSE are handled via `wwwroot/js/rpgroller-api.js` interop.
|
||||
- UI state is maintained server-side per circuit with session/tab persistence for campaign + screen selection.
|
||||
|
||||
Reference in New Issue
Block a user