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
|
- 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.
|
- 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.
|
- 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
|
## UX Checklist
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ Frontend:
|
|||||||
- `RpgRoller/Components/Pages/Home.Models.cs`: reusable `FormState<TModel>` + page form models
|
- `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`
|
- `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
|
- 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/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/js/rpgroller-api.js`: browser-side API + SSE + session storage interop for Blazor
|
||||||
- `RpgRoller/wwwroot/styles.css`: responsive UX styling and theme tokens
|
- `RpgRoller/wwwroot/styles.css`: responsive UX styling and theme tokens
|
||||||
|
|||||||
@@ -153,16 +153,10 @@ public partial class Home
|
|||||||
LastRoll = null;
|
LastRoll = null;
|
||||||
ShowCreateCharacterModal = false;
|
ShowCreateCharacterModal = false;
|
||||||
ShowEditCharacterModal = false;
|
ShowEditCharacterModal = false;
|
||||||
ShowCreateSkillModal = false;
|
|
||||||
ShowEditSkillModal = false;
|
|
||||||
CreateCharacterInitialModel = new();
|
CreateCharacterInitialModel = new();
|
||||||
EditCharacterInitialModel = new();
|
EditCharacterInitialModel = new();
|
||||||
CreateSkillInitialModel = new();
|
|
||||||
EditSkillInitialModel = new();
|
|
||||||
CreateCharacterFormVersion = 0;
|
CreateCharacterFormVersion = 0;
|
||||||
EditCharacterFormVersion = 0;
|
EditCharacterFormVersion = 0;
|
||||||
CreateSkillFormVersion = 0;
|
|
||||||
EditSkillFormVersion = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetStatus(string message, bool isError)
|
private void SetStatus(string message, bool isError)
|
||||||
|
|||||||
@@ -4,49 +4,8 @@ namespace RpgRoller.Components.Pages;
|
|||||||
|
|
||||||
public partial class Home
|
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 _)
|
private async Task OnSkillCreatedAsync(Guid _)
|
||||||
{
|
{
|
||||||
CloseSkillModals();
|
|
||||||
await RefreshCampaignScopeAsync();
|
await RefreshCampaignScopeAsync();
|
||||||
SetStatus("Skill created.", false);
|
SetStatus("Skill created.", false);
|
||||||
}
|
}
|
||||||
@@ -54,7 +13,6 @@ public partial class Home
|
|||||||
private async Task OnSkillUpdatedAsync(Guid skillId)
|
private async Task OnSkillUpdatedAsync(Guid skillId)
|
||||||
{
|
{
|
||||||
SelectedSkillId = skillId;
|
SelectedSkillId = skillId;
|
||||||
CloseSkillModals();
|
|
||||||
await RefreshCampaignScopeAsync();
|
await RefreshCampaignScopeAsync();
|
||||||
SetStatus("Skill updated.", false);
|
SetStatus("Skill updated.", false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,18 +41,11 @@ public partial class Home
|
|||||||
|
|
||||||
private bool ShowCreateCharacterModal { get; set; }
|
private bool ShowCreateCharacterModal { get; set; }
|
||||||
private bool ShowEditCharacterModal { get; set; }
|
private bool ShowEditCharacterModal { get; set; }
|
||||||
private bool ShowCreateSkillModal { get; set; }
|
|
||||||
private bool ShowEditSkillModal { get; set; }
|
|
||||||
private Guid? EditingCharacterId { get; set; }
|
private Guid? EditingCharacterId { get; set; }
|
||||||
private Guid? EditingSkillId { get; set; }
|
|
||||||
private CharacterFormModel CreateCharacterInitialModel { get; set; } = new();
|
private CharacterFormModel CreateCharacterInitialModel { get; set; } = new();
|
||||||
private CharacterFormModel EditCharacterInitialModel { 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 CreateCharacterFormVersion { get; set; }
|
||||||
private int EditCharacterFormVersion { get; set; }
|
private int EditCharacterFormVersion { get; set; }
|
||||||
private int CreateSkillFormVersion { get; set; }
|
|
||||||
private int EditSkillFormVersion { get; set; }
|
|
||||||
private bool StateRefreshInProgress { get; set; }
|
private bool StateRefreshInProgress { get; set; }
|
||||||
private bool HasInteractiveRenderStarted { get; set; }
|
private bool HasInteractiveRenderStarted { get; set; }
|
||||||
private DotNetObjectReference<Home>? DotNetRef { get; set; }
|
private DotNetObjectReference<Home>? DotNetRef { get; set; }
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
SelectedCharacterSkills="SelectedCharacterSkills"
|
SelectedCharacterSkills="SelectedCharacterSkills"
|
||||||
SelectedSkillId="SelectedSkillId"
|
SelectedSkillId="SelectedSkillId"
|
||||||
SelectedSkill="SelectedSkill"
|
SelectedSkill="SelectedSkill"
|
||||||
|
IsD6="IsSelectedCampaignD6"
|
||||||
RollVisibility="RollVisibility"
|
RollVisibility="RollVisibility"
|
||||||
RollVisibilityChanged="OnRollVisibilityChanged"
|
RollVisibilityChanged="OnRollVisibilityChanged"
|
||||||
LastRoll="LastRoll"
|
LastRoll="LastRoll"
|
||||||
@@ -85,8 +86,8 @@
|
|||||||
CharacterSelected="SelectCharacterAsync"
|
CharacterSelected="SelectCharacterAsync"
|
||||||
SkillSelected="SelectSkill"
|
SkillSelected="SelectSkill"
|
||||||
EditCharacterRequested="OpenEditCharacterModal"
|
EditCharacterRequested="OpenEditCharacterModal"
|
||||||
CreateSkillRequested="OpenCreateSkillModal"
|
SkillCreated="OnSkillCreatedAsync"
|
||||||
EditSkillRequested="OpenEditSkillModal"
|
SkillUpdated="OnSkillUpdatedAsync"
|
||||||
RollRequested="RollSelectedSkillAsync" />
|
RollRequested="RollSelectedSkillAsync" />
|
||||||
|
|
||||||
<CampaignLogPanel
|
<CampaignLogPanel
|
||||||
@@ -153,37 +154,3 @@
|
|||||||
IsMutating="IsMutating"
|
IsMutating="IsMutating"
|
||||||
CharacterSaved="OnCharacterUpdatedAsync"
|
CharacterSaved="OnCharacterUpdatedAsync"
|
||||||
CancelRequested="CloseCharacterModals" />
|
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 System.Diagnostics.CodeAnalysis
|
||||||
|
@using RpgRoller.Components.Pages
|
||||||
@using RpgRoller.Contracts
|
@using RpgRoller.Contracts
|
||||||
@attribute [ExcludeFromCodeCoverage]
|
@attribute [ExcludeFromCodeCoverage]
|
||||||
|
|
||||||
@@ -43,8 +44,8 @@
|
|||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h3>Skills</h3>
|
<h3>Skills</h3>
|
||||||
<div class="inline-actions">
|
<div class="inline-actions">
|
||||||
<button type="button" disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))" @onclick="CreateSkillRequested">Create 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="EditSkillRequested">Edit Skill</button>
|
<button type="button" disabled="@(IsMutating || SelectedSkill is null || !CanEditSkill(SelectedSkill))" @onclick="OpenEditSkillModal">Edit Skill</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (SelectedCharacterSkills.Count == 0)
|
@if (SelectedCharacterSkills.Count == 0)
|
||||||
@@ -90,7 +91,49 @@
|
|||||||
</article>
|
</article>
|
||||||
</section>
|
</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 {
|
@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]
|
[Parameter]
|
||||||
public bool IsCampaignDataLoading { get; set; }
|
public bool IsCampaignDataLoading { get; set; }
|
||||||
|
|
||||||
@@ -115,6 +158,9 @@
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public SkillSummary? SelectedSkill { get; set; }
|
public SkillSummary? SelectedSkill { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsD6 { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string RollVisibility { get; set; } = "public";
|
public string RollVisibility { get; set; } = "public";
|
||||||
|
|
||||||
@@ -149,14 +195,67 @@
|
|||||||
public EventCallback<CharacterSummary> EditCharacterRequested { get; set; }
|
public EventCallback<CharacterSummary> EditCharacterRequested { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback CreateSkillRequested { get; set; }
|
public EventCallback<Guid> SkillCreated { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback EditSkillRequested { get; set; }
|
public EventCallback<Guid> SkillUpdated { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback RollRequested { get; set; }
|
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)
|
private async Task OnRollVisibilityChangedAsync(ChangeEventArgs args)
|
||||||
{
|
{
|
||||||
var selectedVisibility = args.Value?.ToString() ?? "public";
|
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.
|
- 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`.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- UI state is maintained server-side per circuit with session/tab persistence for campaign + screen selection.
|
||||||
|
|||||||
Reference in New Issue
Block a user