Move form state ownership from Home to leaf controls
This commit is contained in:
4
FAQ.md
4
FAQ.md
@@ -71,3 +71,7 @@ There is no separate activate button in Play. The selected character in the char
|
|||||||
## Where did the Home page logic move after the refactor?
|
## Where did the Home page logic move after the refactor?
|
||||||
|
|
||||||
`Home.razor` now focuses on composition and delegates behavior to concern-based code-behind partials (`Home.Auth.cs`, `Home.Campaign.cs`, `Home.Character.cs`, `Home.Skill.cs`, etc.) plus dedicated UI controls under `Components/Pages/HomeControls/`.
|
`Home.razor` now focuses on composition and delegates behavior to concern-based code-behind partials (`Home.Auth.cs`, `Home.Campaign.cs`, `Home.Character.cs`, `Home.Skill.cs`, etc.) plus dedicated UI controls under `Components/Pages/HomeControls/`.
|
||||||
|
|
||||||
|
## Why is auth form state kept in `AuthSection` instead of `Home`?
|
||||||
|
|
||||||
|
Auth inputs and inline validation are transient UI concerns, so they now live in `AuthSection`. `Home` keeps only session/workspace state and API workflows, and returns `FormSubmissionResult` back to controls for display.
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ Frontend:
|
|||||||
- `RpgRoller/Components/Pages/Home.*.cs`: concern-based partial class split (`State`, `Lifecycle`, `Auth`, `Campaign`, `Character`, `Skill`, `Realtime`, `Api`, `Presentation`, `Validation`)
|
- `RpgRoller/Components/Pages/Home.*.cs`: concern-based partial class split (`State`, `Lifecycle`, `Auth`, `Campaign`, `Character`, `Skill`, `Realtime`, `Api`, `Presentation`, `Validation`)
|
||||||
- `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; `Home` receives typed submissions and returns `FormSubmissionResult` for server/business validation feedback
|
||||||
- `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
|
||||||
|
|
||||||
|
|||||||
@@ -4,23 +4,25 @@ namespace RpgRoller.Components.Pages;
|
|||||||
|
|
||||||
public partial class Home
|
public partial class Home
|
||||||
{
|
{
|
||||||
private async Task RegisterAsync()
|
private async Task<FormSubmissionResult> RegisterAsync(RegisterFormModel model)
|
||||||
{
|
{
|
||||||
RegisterState.ResetValidation();
|
var validationErrors = new Dictionary<string, string>();
|
||||||
var model = RegisterState.Model;
|
|
||||||
|
|
||||||
AddRequiredError(RegisterState.Errors, "username", model.Username, "Username is required.");
|
AddRequiredError(validationErrors, "username", model.Username, "Username is required.");
|
||||||
AddRequiredError(RegisterState.Errors, "displayName", model.DisplayName, "Display name is required.");
|
AddRequiredError(validationErrors, "displayName", model.DisplayName, "Display name is required.");
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(model.Password) || model.Password.Length < 8)
|
if (string.IsNullOrWhiteSpace(model.Password) || model.Password.Length < 8)
|
||||||
{
|
{
|
||||||
RegisterState.Errors["password"] = "Password must be at least 8 characters.";
|
validationErrors["password"] = "Password must be at least 8 characters.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RegisterState.Errors.Count > 0)
|
if (validationErrors.Count > 0)
|
||||||
{
|
{
|
||||||
RegisterState.ErrorMessage = "Resolve validation issues before submitting.";
|
return new FormSubmissionResult
|
||||||
return;
|
{
|
||||||
|
ErrorMessage = "Resolve validation issues before submitting.",
|
||||||
|
Errors = validationErrors
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
IsMutating = true;
|
IsMutating = true;
|
||||||
@@ -31,18 +33,23 @@ public partial class Home
|
|||||||
"/api/auth/register",
|
"/api/auth/register",
|
||||||
new RegisterRequest(model.Username.Trim(), model.Password, model.DisplayName.Trim()));
|
new RegisterRequest(model.Username.Trim(), model.Password, model.DisplayName.Trim()));
|
||||||
|
|
||||||
model.Password = string.Empty;
|
|
||||||
SetStatus("Registration successful. You can log in now.", false);
|
SetStatus("Registration successful. You can log in now.", false);
|
||||||
|
return new FormSubmissionResult();
|
||||||
}
|
}
|
||||||
catch (ApiRequestException ex)
|
catch (ApiRequestException ex)
|
||||||
{
|
{
|
||||||
if (ex.Message.Contains("already taken", StringComparison.OrdinalIgnoreCase))
|
if (ex.Message.Contains("already taken", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
RegisterState.Errors["username"] = "Username is already taken. Choose another one.";
|
return new FormSubmissionResult
|
||||||
return;
|
{
|
||||||
|
Errors = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["username"] = "Username is already taken. Choose another one."
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
RegisterState.ErrorMessage = ex.Message;
|
return new FormSubmissionResult { ErrorMessage = ex.Message };
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -50,18 +57,20 @@ public partial class Home
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoginAsync()
|
private async Task<FormSubmissionResult> LoginAsync(LoginFormModel model)
|
||||||
{
|
{
|
||||||
LoginState.ResetValidation();
|
var validationErrors = new Dictionary<string, string>();
|
||||||
var model = LoginState.Model;
|
|
||||||
|
|
||||||
AddRequiredError(LoginState.Errors, "username", model.Username, "Username is required.");
|
AddRequiredError(validationErrors, "username", model.Username, "Username is required.");
|
||||||
AddRequiredError(LoginState.Errors, "password", model.Password, "Password is required.");
|
AddRequiredError(validationErrors, "password", model.Password, "Password is required.");
|
||||||
|
|
||||||
if (LoginState.Errors.Count > 0)
|
if (validationErrors.Count > 0)
|
||||||
{
|
{
|
||||||
LoginState.ErrorMessage = "Resolve validation issues before submitting.";
|
return new FormSubmissionResult
|
||||||
return;
|
{
|
||||||
|
ErrorMessage = "Resolve validation issues before submitting.",
|
||||||
|
Errors = validationErrors
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
IsMutating = true;
|
IsMutating = true;
|
||||||
@@ -72,13 +81,13 @@ public partial class Home
|
|||||||
"/api/auth/login",
|
"/api/auth/login",
|
||||||
new LoginRequest(model.Username.Trim(), model.Password));
|
new LoginRequest(model.Username.Trim(), model.Password));
|
||||||
|
|
||||||
model.Password = string.Empty;
|
|
||||||
await ReloadAuthenticatedSessionAsync(null);
|
await ReloadAuthenticatedSessionAsync(null);
|
||||||
SetStatus("Logged in.", false);
|
SetStatus("Logged in.", false);
|
||||||
|
return new FormSubmissionResult();
|
||||||
}
|
}
|
||||||
catch (ApiRequestException ex)
|
catch (ApiRequestException ex)
|
||||||
{
|
{
|
||||||
LoginState.ErrorMessage = ex.Message;
|
return new FormSubmissionResult { ErrorMessage = ex.Message };
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -51,18 +51,20 @@ public partial class Home
|
|||||||
await SyncStateEventsAsync();
|
await SyncStateEventsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CreateCampaignAsync()
|
private async Task<FormSubmissionResult> CreateCampaignAsync(CampaignFormModel model)
|
||||||
{
|
{
|
||||||
CampaignState.ResetValidation();
|
var validationErrors = new Dictionary<string, string>();
|
||||||
var model = CampaignState.Model;
|
|
||||||
|
|
||||||
AddRequiredError(CampaignState.Errors, "name", model.Name, "Campaign name is required.");
|
AddRequiredError(validationErrors, "name", model.Name, "Campaign name is required.");
|
||||||
AddRequiredError(CampaignState.Errors, "rulesetId", model.RulesetId, "Ruleset is required.");
|
AddRequiredError(validationErrors, "rulesetId", model.RulesetId, "Ruleset is required.");
|
||||||
|
|
||||||
if (CampaignState.Errors.Count > 0)
|
if (validationErrors.Count > 0)
|
||||||
{
|
{
|
||||||
CampaignState.ErrorMessage = "Resolve validation issues before submitting.";
|
return new FormSubmissionResult
|
||||||
return;
|
{
|
||||||
|
ErrorMessage = "Resolve validation issues before submitting.",
|
||||||
|
Errors = validationErrors
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
IsMutating = true;
|
IsMutating = true;
|
||||||
@@ -73,15 +75,15 @@ public partial class Home
|
|||||||
"/api/campaigns",
|
"/api/campaigns",
|
||||||
new CreateCampaignRequest(model.Name.Trim(), model.RulesetId));
|
new CreateCampaignRequest(model.Name.Trim(), model.RulesetId));
|
||||||
|
|
||||||
model.Name = string.Empty;
|
|
||||||
await ReloadCampaignsAsync(campaign.Id);
|
await ReloadCampaignsAsync(campaign.Id);
|
||||||
await RefreshCampaignScopeAsync();
|
await RefreshCampaignScopeAsync();
|
||||||
await SyncStateEventsAsync();
|
await SyncStateEventsAsync();
|
||||||
SetStatus("Campaign created.", false);
|
SetStatus("Campaign created.", false);
|
||||||
|
return new FormSubmissionResult();
|
||||||
}
|
}
|
||||||
catch (ApiRequestException ex)
|
catch (ApiRequestException ex)
|
||||||
{
|
{
|
||||||
CampaignState.ErrorMessage = ex.Message;
|
return new FormSubmissionResult { ErrorMessage = ex.Message };
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ public partial class Home
|
|||||||
{
|
{
|
||||||
private void OpenCreateCharacterModal()
|
private void OpenCreateCharacterModal()
|
||||||
{
|
{
|
||||||
var model = CreateCharacterState.Model;
|
CreateCharacterInitialModel = new CharacterFormModel
|
||||||
model.Name = string.Empty;
|
{
|
||||||
model.CampaignId = SelectedCampaignId?.ToString() ?? string.Empty;
|
Name = string.Empty,
|
||||||
CreateCharacterState.ResetValidation();
|
CampaignId = SelectedCampaignId?.ToString() ?? string.Empty
|
||||||
|
};
|
||||||
|
CreateCharacterFormVersion++;
|
||||||
ShowCreateCharacterModal = true;
|
ShowCreateCharacterModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,11 +19,12 @@ public partial class Home
|
|||||||
{
|
{
|
||||||
EditingCharacterId = character.Id;
|
EditingCharacterId = character.Id;
|
||||||
|
|
||||||
var model = EditCharacterState.Model;
|
EditCharacterInitialModel = new CharacterFormModel
|
||||||
model.Name = character.Name;
|
{
|
||||||
model.CampaignId = character.CampaignId.ToString();
|
Name = character.Name,
|
||||||
|
CampaignId = character.CampaignId.ToString()
|
||||||
EditCharacterState.ResetValidation();
|
};
|
||||||
|
EditCharacterFormVersion++;
|
||||||
ShowEditCharacterModal = true;
|
ShowEditCharacterModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,23 +35,25 @@ public partial class Home
|
|||||||
EditingCharacterId = null;
|
EditingCharacterId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CreateCharacterAsync()
|
private async Task<FormSubmissionResult> CreateCharacterAsync(CharacterFormModel model)
|
||||||
{
|
{
|
||||||
CreateCharacterState.ResetValidation();
|
var validationErrors = new Dictionary<string, string>();
|
||||||
var model = CreateCharacterState.Model;
|
|
||||||
|
|
||||||
AddRequiredError(CreateCharacterState.Errors, "name", model.Name, "Character name is required.");
|
AddRequiredError(validationErrors, "name", model.Name, "Character name is required.");
|
||||||
var hasCampaignId = TryParseGuid(
|
var hasCampaignId = TryParseGuid(
|
||||||
model.CampaignId,
|
model.CampaignId,
|
||||||
CreateCharacterState.Errors,
|
validationErrors,
|
||||||
"campaignId",
|
"campaignId",
|
||||||
"Campaign is required.",
|
"Campaign is required.",
|
||||||
out var campaignId);
|
out var campaignId);
|
||||||
|
|
||||||
if (CreateCharacterState.Errors.Count > 0 || !hasCampaignId)
|
if (validationErrors.Count > 0 || !hasCampaignId)
|
||||||
{
|
{
|
||||||
CreateCharacterState.ErrorMessage = "Resolve validation issues before submitting.";
|
return new FormSubmissionResult
|
||||||
return;
|
{
|
||||||
|
ErrorMessage = "Resolve validation issues before submitting.",
|
||||||
|
Errors = validationErrors
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
IsMutating = true;
|
IsMutating = true;
|
||||||
@@ -64,10 +69,11 @@ public partial class Home
|
|||||||
await RefreshCampaignScopeAsync();
|
await RefreshCampaignScopeAsync();
|
||||||
await SyncStateEventsAsync();
|
await SyncStateEventsAsync();
|
||||||
SetStatus("Character created.", false);
|
SetStatus("Character created.", false);
|
||||||
|
return new FormSubmissionResult();
|
||||||
}
|
}
|
||||||
catch (ApiRequestException ex)
|
catch (ApiRequestException ex)
|
||||||
{
|
{
|
||||||
CreateCharacterState.ErrorMessage = ex.Message;
|
return new FormSubmissionResult { ErrorMessage = ex.Message };
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -75,29 +81,29 @@ public partial class Home
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateCharacterAsync()
|
private async Task<FormSubmissionResult> UpdateCharacterAsync(CharacterFormModel model)
|
||||||
{
|
{
|
||||||
EditCharacterState.ResetValidation();
|
|
||||||
|
|
||||||
if (!EditingCharacterId.HasValue)
|
if (!EditingCharacterId.HasValue)
|
||||||
{
|
{
|
||||||
EditCharacterState.ErrorMessage = "No character selected.";
|
return new FormSubmissionResult { ErrorMessage = "No character selected." };
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var model = EditCharacterState.Model;
|
var validationErrors = new Dictionary<string, string>();
|
||||||
AddRequiredError(EditCharacterState.Errors, "name", model.Name, "Character name is required.");
|
AddRequiredError(validationErrors, "name", model.Name, "Character name is required.");
|
||||||
var hasCampaignId = TryParseGuid(
|
var hasCampaignId = TryParseGuid(
|
||||||
model.CampaignId,
|
model.CampaignId,
|
||||||
EditCharacterState.Errors,
|
validationErrors,
|
||||||
"campaignId",
|
"campaignId",
|
||||||
"Campaign is required.",
|
"Campaign is required.",
|
||||||
out var campaignId);
|
out var campaignId);
|
||||||
|
|
||||||
if (EditCharacterState.Errors.Count > 0 || !hasCampaignId)
|
if (validationErrors.Count > 0 || !hasCampaignId)
|
||||||
{
|
{
|
||||||
EditCharacterState.ErrorMessage = "Resolve validation issues before submitting.";
|
return new FormSubmissionResult
|
||||||
return;
|
{
|
||||||
|
ErrorMessage = "Resolve validation issues before submitting.",
|
||||||
|
Errors = validationErrors
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
IsMutating = true;
|
IsMutating = true;
|
||||||
@@ -113,10 +119,11 @@ public partial class Home
|
|||||||
await RefreshCampaignScopeAsync();
|
await RefreshCampaignScopeAsync();
|
||||||
await SyncStateEventsAsync();
|
await SyncStateEventsAsync();
|
||||||
SetStatus("Character updated.", false);
|
SetStatus("Character updated.", false);
|
||||||
|
return new FormSubmissionResult();
|
||||||
}
|
}
|
||||||
catch (ApiRequestException ex)
|
catch (ApiRequestException ex)
|
||||||
{
|
{
|
||||||
EditCharacterState.ErrorMessage = ex.Message;
|
return new FormSubmissionResult { ErrorMessage = ex.Message };
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -80,10 +80,6 @@ public partial class Home
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Rulesets = (await RequestAsync<IReadOnlyList<RulesetDefinition>>("GET", "/api/rulesets")).ToList();
|
Rulesets = (await RequestAsync<IReadOnlyList<RulesetDefinition>>("GET", "/api/rulesets")).ToList();
|
||||||
if (Rulesets.Count > 0 && string.IsNullOrWhiteSpace(CampaignState.Model.RulesetId))
|
|
||||||
{
|
|
||||||
CampaignState.Model.RulesetId = Rulesets[0].Id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (ApiRequestException ex)
|
catch (ApiRequestException ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ public sealed class FormState<TModel>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class FormSubmissionResult
|
||||||
|
{
|
||||||
|
public Dictionary<string, string> Errors { get; init; } = [];
|
||||||
|
public string? ErrorMessage { get; init; }
|
||||||
|
public bool IsSuccess => Errors.Count == 0 && string.IsNullOrWhiteSpace(ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class RegisterFormModel
|
public sealed class RegisterFormModel
|
||||||
{
|
{
|
||||||
public string Username { get; set; } = string.Empty;
|
public string Username { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -155,6 +155,14 @@ public partial class Home
|
|||||||
ShowEditCharacterModal = false;
|
ShowEditCharacterModal = false;
|
||||||
ShowCreateSkillModal = false;
|
ShowCreateSkillModal = false;
|
||||||
ShowEditSkillModal = 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)
|
private void SetStatus(string message, bool isError)
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ public partial class Home
|
|||||||
{
|
{
|
||||||
private void OpenCreateSkillModal()
|
private void OpenCreateSkillModal()
|
||||||
{
|
{
|
||||||
var model = CreateSkillState.Model;
|
CreateSkillInitialModel = new SkillFormModel
|
||||||
model.Name = string.Empty;
|
{
|
||||||
model.DiceRollDefinition = string.Empty;
|
Name = string.Empty,
|
||||||
model.WildDice = IsSelectedCampaignD6 ? 1 : 0;
|
DiceRollDefinition = string.Empty,
|
||||||
model.AllowFumble = IsSelectedCampaignD6;
|
WildDice = IsSelectedCampaignD6 ? 1 : 0,
|
||||||
|
AllowFumble = IsSelectedCampaignD6
|
||||||
CreateSkillState.ResetValidation();
|
};
|
||||||
|
CreateSkillFormVersion++;
|
||||||
ShowCreateSkillModal = true;
|
ShowCreateSkillModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,13 +26,14 @@ public partial class Home
|
|||||||
|
|
||||||
EditingSkillId = SelectedSkill.Id;
|
EditingSkillId = SelectedSkill.Id;
|
||||||
|
|
||||||
var model = EditSkillState.Model;
|
EditSkillInitialModel = new SkillFormModel
|
||||||
model.Name = SelectedSkill.Name;
|
{
|
||||||
model.DiceRollDefinition = SelectedSkill.DiceRollDefinition;
|
Name = SelectedSkill.Name,
|
||||||
model.WildDice = SelectedSkill.WildDice;
|
DiceRollDefinition = SelectedSkill.DiceRollDefinition,
|
||||||
model.AllowFumble = SelectedSkill.AllowFumble;
|
WildDice = SelectedSkill.WildDice,
|
||||||
|
AllowFumble = SelectedSkill.AllowFumble
|
||||||
EditSkillState.ResetValidation();
|
};
|
||||||
|
EditSkillFormVersion++;
|
||||||
ShowEditSkillModal = true;
|
ShowEditSkillModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,29 +44,29 @@ public partial class Home
|
|||||||
EditingSkillId = null;
|
EditingSkillId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CreateSkillAsync()
|
private async Task<FormSubmissionResult> CreateSkillAsync(SkillFormModel model)
|
||||||
{
|
{
|
||||||
CreateSkillState.ResetValidation();
|
|
||||||
|
|
||||||
if (SelectedCharacter is null)
|
if (SelectedCharacter is null)
|
||||||
{
|
{
|
||||||
CreateSkillState.ErrorMessage = "Select a character first.";
|
return new FormSubmissionResult { ErrorMessage = "Select a character first." };
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var model = CreateSkillState.Model;
|
var validationErrors = new Dictionary<string, string>();
|
||||||
AddRequiredError(CreateSkillState.Errors, "name", model.Name, "Skill name is required.");
|
AddRequiredError(validationErrors, "name", model.Name, "Skill name is required.");
|
||||||
AddRequiredError(CreateSkillState.Errors, "diceRollDefinition", model.DiceRollDefinition, "Expression is required.");
|
AddRequiredError(validationErrors, "diceRollDefinition", model.DiceRollDefinition, "Expression is required.");
|
||||||
|
|
||||||
if (IsSelectedCampaignD6 && model.WildDice < 1)
|
if (IsSelectedCampaignD6 && model.WildDice < 1)
|
||||||
{
|
{
|
||||||
CreateSkillState.Errors["wildDice"] = "D6 skills require at least one wild die.";
|
validationErrors["wildDice"] = "D6 skills require at least one wild die.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CreateSkillState.Errors.Count > 0)
|
if (validationErrors.Count > 0)
|
||||||
{
|
{
|
||||||
CreateSkillState.ErrorMessage = "Resolve validation issues before submitting.";
|
return new FormSubmissionResult
|
||||||
return;
|
{
|
||||||
|
ErrorMessage = "Resolve validation issues before submitting.",
|
||||||
|
Errors = validationErrors
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
IsMutating = true;
|
IsMutating = true;
|
||||||
@@ -78,10 +80,11 @@ public partial class Home
|
|||||||
CloseSkillModals();
|
CloseSkillModals();
|
||||||
await RefreshCampaignScopeAsync();
|
await RefreshCampaignScopeAsync();
|
||||||
SetStatus("Skill created.", false);
|
SetStatus("Skill created.", false);
|
||||||
|
return new FormSubmissionResult();
|
||||||
}
|
}
|
||||||
catch (ApiRequestException ex)
|
catch (ApiRequestException ex)
|
||||||
{
|
{
|
||||||
CreateSkillState.ErrorMessage = ex.Message;
|
return new FormSubmissionResult { ErrorMessage = ex.Message };
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -89,29 +92,29 @@ public partial class Home
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateSkillAsync()
|
private async Task<FormSubmissionResult> UpdateSkillAsync(SkillFormModel model)
|
||||||
{
|
{
|
||||||
EditSkillState.ResetValidation();
|
|
||||||
|
|
||||||
if (!EditingSkillId.HasValue)
|
if (!EditingSkillId.HasValue)
|
||||||
{
|
{
|
||||||
EditSkillState.ErrorMessage = "No skill selected.";
|
return new FormSubmissionResult { ErrorMessage = "No skill selected." };
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var model = EditSkillState.Model;
|
var validationErrors = new Dictionary<string, string>();
|
||||||
AddRequiredError(EditSkillState.Errors, "name", model.Name, "Skill name is required.");
|
AddRequiredError(validationErrors, "name", model.Name, "Skill name is required.");
|
||||||
AddRequiredError(EditSkillState.Errors, "diceRollDefinition", model.DiceRollDefinition, "Expression is required.");
|
AddRequiredError(validationErrors, "diceRollDefinition", model.DiceRollDefinition, "Expression is required.");
|
||||||
|
|
||||||
if (IsSelectedCampaignD6 && model.WildDice < 1)
|
if (IsSelectedCampaignD6 && model.WildDice < 1)
|
||||||
{
|
{
|
||||||
EditSkillState.Errors["wildDice"] = "D6 skills require at least one wild die.";
|
validationErrors["wildDice"] = "D6 skills require at least one wild die.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (EditSkillState.Errors.Count > 0)
|
if (validationErrors.Count > 0)
|
||||||
{
|
{
|
||||||
EditSkillState.ErrorMessage = "Resolve validation issues before submitting.";
|
return new FormSubmissionResult
|
||||||
return;
|
{
|
||||||
|
ErrorMessage = "Resolve validation issues before submitting.",
|
||||||
|
Errors = validationErrors
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
IsMutating = true;
|
IsMutating = true;
|
||||||
@@ -126,10 +129,11 @@ public partial class Home
|
|||||||
CloseSkillModals();
|
CloseSkillModals();
|
||||||
await RefreshCampaignScopeAsync();
|
await RefreshCampaignScopeAsync();
|
||||||
SetStatus("Skill updated.", false);
|
SetStatus("Skill updated.", false);
|
||||||
|
return new FormSubmissionResult();
|
||||||
}
|
}
|
||||||
catch (ApiRequestException ex)
|
catch (ApiRequestException ex)
|
||||||
{
|
{
|
||||||
EditSkillState.ErrorMessage = ex.Message;
|
return new FormSubmissionResult { ErrorMessage = ex.Message };
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,14 +17,6 @@ public partial class Home
|
|||||||
private const string MobilePanelSessionKey = "play-panel";
|
private const string MobilePanelSessionKey = "play-panel";
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
private FormState<RegisterFormModel> RegisterState { get; } = new();
|
|
||||||
private FormState<LoginFormModel> LoginState { get; } = new();
|
|
||||||
private FormState<CampaignFormModel> CampaignState { get; } = new();
|
|
||||||
private FormState<CharacterFormModel> CreateCharacterState { get; } = new();
|
|
||||||
private FormState<CharacterFormModel> EditCharacterState { get; } = new();
|
|
||||||
private FormState<SkillFormModel> CreateSkillState { get; } = new();
|
|
||||||
private FormState<SkillFormModel> EditSkillState { get; } = new();
|
|
||||||
|
|
||||||
private UserSummary? User { get; set; }
|
private UserSummary? User { get; set; }
|
||||||
private Guid? ActiveCharacterId { get; set; }
|
private Guid? ActiveCharacterId { get; set; }
|
||||||
private Guid? SelectedCampaignId { get; set; }
|
private Guid? SelectedCampaignId { get; set; }
|
||||||
@@ -55,6 +47,14 @@ public partial class Home
|
|||||||
private bool ShowEditSkillModal { get; set; }
|
private bool ShowEditSkillModal { get; set; }
|
||||||
private Guid? EditingCharacterId { get; set; }
|
private Guid? EditingCharacterId { get; set; }
|
||||||
private Guid? EditingSkillId { 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 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; }
|
||||||
|
|||||||
@@ -27,8 +27,6 @@
|
|||||||
|
|
||||||
case HomeViewMode.Anonymous:
|
case HomeViewMode.Anonymous:
|
||||||
<AuthSection
|
<AuthSection
|
||||||
RegisterState="RegisterState"
|
|
||||||
LoginState="LoginState"
|
|
||||||
IsMutating="IsMutating"
|
IsMutating="IsMutating"
|
||||||
StatusMessage="StatusMessage"
|
StatusMessage="StatusMessage"
|
||||||
StatusIsError="StatusIsError"
|
StatusIsError="StatusIsError"
|
||||||
@@ -116,7 +114,6 @@
|
|||||||
SelectedCampaignId="SelectedCampaignId"
|
SelectedCampaignId="SelectedCampaignId"
|
||||||
SelectedCampaignName="SelectedCampaignName"
|
SelectedCampaignName="SelectedCampaignName"
|
||||||
SelectedCampaign="SelectedCampaign"
|
SelectedCampaign="SelectedCampaign"
|
||||||
CampaignState="CampaignState"
|
|
||||||
Rulesets="Rulesets"
|
Rulesets="Rulesets"
|
||||||
IsMutating="IsMutating"
|
IsMutating="IsMutating"
|
||||||
OwnerLabel="OwnerLabel"
|
OwnerLabel="OwnerLabel"
|
||||||
@@ -137,7 +134,8 @@
|
|||||||
SubmitLabel="Create Character"
|
SubmitLabel="Create Character"
|
||||||
NameInputId="character-create-name"
|
NameInputId="character-create-name"
|
||||||
CampaignInputId="character-create-campaign"
|
CampaignInputId="character-create-campaign"
|
||||||
FormState="CreateCharacterState"
|
InitialModel="CreateCharacterInitialModel"
|
||||||
|
FormVersion="CreateCharacterFormVersion"
|
||||||
Campaigns="Campaigns"
|
Campaigns="Campaigns"
|
||||||
IsMutating="IsMutating"
|
IsMutating="IsMutating"
|
||||||
SubmitRequested="CreateCharacterAsync"
|
SubmitRequested="CreateCharacterAsync"
|
||||||
@@ -149,7 +147,8 @@
|
|||||||
SubmitLabel="Save Character"
|
SubmitLabel="Save Character"
|
||||||
NameInputId="character-edit-name"
|
NameInputId="character-edit-name"
|
||||||
CampaignInputId="character-edit-campaign"
|
CampaignInputId="character-edit-campaign"
|
||||||
FormState="EditCharacterState"
|
InitialModel="EditCharacterInitialModel"
|
||||||
|
FormVersion="EditCharacterFormVersion"
|
||||||
Campaigns="Campaigns"
|
Campaigns="Campaigns"
|
||||||
IsMutating="IsMutating"
|
IsMutating="IsMutating"
|
||||||
SubmitRequested="UpdateCharacterAsync"
|
SubmitRequested="UpdateCharacterAsync"
|
||||||
@@ -164,7 +163,8 @@
|
|||||||
ExpressionInputId="skill-create-expression"
|
ExpressionInputId="skill-create-expression"
|
||||||
WildDiceInputId="skill-create-wild-dice"
|
WildDiceInputId="skill-create-wild-dice"
|
||||||
AllowFumbleInputId="skill-create-allow-fumble"
|
AllowFumbleInputId="skill-create-allow-fumble"
|
||||||
FormState="CreateSkillState"
|
InitialModel="CreateSkillInitialModel"
|
||||||
|
FormVersion="CreateSkillFormVersion"
|
||||||
IsMutating="IsMutating"
|
IsMutating="IsMutating"
|
||||||
SubmitRequested="CreateSkillAsync"
|
SubmitRequested="CreateSkillAsync"
|
||||||
CancelRequested="CloseSkillModals" />
|
CancelRequested="CloseSkillModals" />
|
||||||
@@ -178,7 +178,8 @@
|
|||||||
ExpressionInputId="skill-edit-expression"
|
ExpressionInputId="skill-edit-expression"
|
||||||
WildDiceInputId="skill-edit-wild-dice"
|
WildDiceInputId="skill-edit-wild-dice"
|
||||||
AllowFumbleInputId="skill-edit-allow-fumble"
|
AllowFumbleInputId="skill-edit-allow-fumble"
|
||||||
FormState="EditSkillState"
|
InitialModel="EditSkillInitialModel"
|
||||||
|
FormVersion="EditSkillFormVersion"
|
||||||
IsMutating="IsMutating"
|
IsMutating="IsMutating"
|
||||||
SubmitRequested="UpdateSkillAsync"
|
SubmitRequested="UpdateSkillAsync"
|
||||||
CancelRequested="CloseSkillModals" />
|
CancelRequested="CloseSkillModals" />
|
||||||
|
|||||||
@@ -65,11 +65,8 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
private FormState<RegisterFormModel> RegisterState { get; } = new();
|
||||||
public FormState<RegisterFormModel> RegisterState { get; set; } = new();
|
private FormState<LoginFormModel> LoginState { get; } = new();
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public FormState<LoginFormModel> LoginState { get; set; } = new();
|
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsMutating { get; set; }
|
public bool IsMutating { get; set; }
|
||||||
@@ -81,18 +78,55 @@
|
|||||||
public bool StatusIsError { get; set; }
|
public bool StatusIsError { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback RegisterSubmitted { get; set; }
|
public Func<RegisterFormModel, Task<FormSubmissionResult>> RegisterSubmitted { get; set; } = _ => Task.FromResult(new FormSubmissionResult());
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback LoginSubmitted { get; set; }
|
public Func<LoginFormModel, Task<FormSubmissionResult>> LoginSubmitted { get; set; } = _ => Task.FromResult(new FormSubmissionResult());
|
||||||
|
|
||||||
private async Task SubmitRegisterAsync()
|
private async Task SubmitRegisterAsync()
|
||||||
{
|
{
|
||||||
await RegisterSubmitted.InvokeAsync();
|
RegisterState.ResetValidation();
|
||||||
|
|
||||||
|
var result = await RegisterSubmitted.Invoke(new RegisterFormModel
|
||||||
|
{
|
||||||
|
Username = RegisterState.Model.Username,
|
||||||
|
DisplayName = RegisterState.Model.DisplayName,
|
||||||
|
Password = RegisterState.Model.Password
|
||||||
|
});
|
||||||
|
|
||||||
|
ApplyResult(RegisterState, result);
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
RegisterState.Model.Password = string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SubmitLoginAsync()
|
private async Task SubmitLoginAsync()
|
||||||
{
|
{
|
||||||
await LoginSubmitted.InvokeAsync();
|
LoginState.ResetValidation();
|
||||||
|
|
||||||
|
var result = await LoginSubmitted.Invoke(new LoginFormModel
|
||||||
|
{
|
||||||
|
Username = LoginState.Model.Username,
|
||||||
|
Password = LoginState.Model.Password
|
||||||
|
});
|
||||||
|
|
||||||
|
ApplyResult(LoginState, result);
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
LoginState.Model.Password = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyResult<TModel>(FormState<TModel> state, FormSubmissionResult result)
|
||||||
|
where TModel : new()
|
||||||
|
{
|
||||||
|
state.Errors.Clear();
|
||||||
|
foreach (var (key, value) in result.Errors)
|
||||||
|
{
|
||||||
|
state.Errors[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.ErrorMessage = result.ErrorMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
{
|
{
|
||||||
<p class="form-error">@CampaignState.ErrorMessage</p>
|
<p class="form-error">@CampaignState.ErrorMessage</p>
|
||||||
}
|
}
|
||||||
<form class="form-grid" @onsubmit="CreateCampaignSubmitted" @onsubmit:preventDefault>
|
<form class="form-grid" @onsubmit="SubmitCreateCampaignAsync" @onsubmit:preventDefault>
|
||||||
<label for="campaign-name">Campaign name</label>
|
<label for="campaign-name">Campaign name</label>
|
||||||
<input id="campaign-name" @bind="CampaignState.Model.Name" @bind:event="oninput" />
|
<input id="campaign-name" @bind="CampaignState.Model.Name" @bind:event="oninput" />
|
||||||
@if (CampaignState.Errors.TryGetValue("name", out var campaignNameError))
|
@if (CampaignState.Errors.TryGetValue("name", out var campaignNameError))
|
||||||
@@ -98,6 +98,8 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
private FormState<CampaignFormModel> CampaignState { get; } = new();
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IReadOnlyList<CampaignSummary> Campaigns { get; set; } = [];
|
public IReadOnlyList<CampaignSummary> Campaigns { get; set; } = [];
|
||||||
|
|
||||||
@@ -110,9 +112,6 @@
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public CampaignDetails? SelectedCampaign { get; set; }
|
public CampaignDetails? SelectedCampaign { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public FormState<CampaignFormModel> CampaignState { get; set; } = new();
|
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IReadOnlyList<RulesetDefinition> Rulesets { get; set; } = [];
|
public IReadOnlyList<RulesetDefinition> Rulesets { get; set; } = [];
|
||||||
|
|
||||||
@@ -129,11 +128,42 @@
|
|||||||
public EventCallback<ChangeEventArgs> CampaignSelectionChanged { get; set; }
|
public EventCallback<ChangeEventArgs> CampaignSelectionChanged { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback CreateCampaignSubmitted { get; set; }
|
public Func<CampaignFormModel, Task<FormSubmissionResult>> CreateCampaignSubmitted { get; set; } = _ => Task.FromResult(new FormSubmissionResult());
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback CreateCharacterRequested { get; set; }
|
public EventCallback CreateCharacterRequested { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<CharacterSummary> EditCharacterRequested { get; set; }
|
public EventCallback<CharacterSummary> EditCharacterRequested { get; set; }
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(CampaignState.Model.RulesetId) && Rulesets.Count > 0)
|
||||||
|
{
|
||||||
|
CampaignState.Model.RulesetId = Rulesets[0].Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SubmitCreateCampaignAsync()
|
||||||
|
{
|
||||||
|
CampaignState.ResetValidation();
|
||||||
|
|
||||||
|
var result = await CreateCampaignSubmitted.Invoke(new CampaignFormModel
|
||||||
|
{
|
||||||
|
Name = CampaignState.Model.Name,
|
||||||
|
RulesetId = CampaignState.Model.RulesetId
|
||||||
|
});
|
||||||
|
|
||||||
|
CampaignState.Errors.Clear();
|
||||||
|
foreach (var (key, value) in result.Errors)
|
||||||
|
{
|
||||||
|
CampaignState.Errors[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
CampaignState.ErrorMessage = result.ErrorMessage;
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
CampaignState.Model.Name = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
private FormState<CharacterFormModel> FormState { get; } = new();
|
||||||
|
private int AppliedFormVersion { get; set; } = -1;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool Visible { get; set; }
|
public bool Visible { get; set; }
|
||||||
|
|
||||||
@@ -57,7 +60,10 @@
|
|||||||
public string CampaignInputId { get; set; } = "character-campaign";
|
public string CampaignInputId { get; set; } = "character-campaign";
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public FormState<CharacterFormModel> FormState { get; set; } = new();
|
public CharacterFormModel InitialModel { get; set; } = new();
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int FormVersion { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IReadOnlyList<CampaignSummary> Campaigns { get; set; } = [];
|
public IReadOnlyList<CampaignSummary> Campaigns { get; set; } = [];
|
||||||
@@ -66,13 +72,39 @@
|
|||||||
public bool IsMutating { get; set; }
|
public bool IsMutating { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback SubmitRequested { get; set; }
|
public Func<CharacterFormModel, Task<FormSubmissionResult>> SubmitRequested { get; set; } = _ => Task.FromResult(new FormSubmissionResult());
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback CancelRequested { get; set; }
|
public EventCallback CancelRequested { get; set; }
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
if (!Visible || FormVersion == AppliedFormVersion)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FormState.Model.Name = InitialModel.Name;
|
||||||
|
FormState.Model.CampaignId = InitialModel.CampaignId;
|
||||||
|
FormState.ResetValidation();
|
||||||
|
AppliedFormVersion = FormVersion;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SubmitAsync()
|
private async Task SubmitAsync()
|
||||||
{
|
{
|
||||||
await SubmitRequested.InvokeAsync();
|
FormState.ResetValidation();
|
||||||
|
|
||||||
|
var result = await SubmitRequested.Invoke(new CharacterFormModel
|
||||||
|
{
|
||||||
|
Name = FormState.Model.Name,
|
||||||
|
CampaignId = FormState.Model.CampaignId
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var (key, value) in result.Errors)
|
||||||
|
{
|
||||||
|
FormState.Errors[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
FormState.ErrorMessage = result.ErrorMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
private FormState<SkillFormModel> FormState { get; } = new();
|
||||||
|
private int AppliedFormVersion { get; set; } = -1;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool Visible { get; set; }
|
public bool Visible { get; set; }
|
||||||
|
|
||||||
@@ -70,19 +73,52 @@
|
|||||||
public string AllowFumbleInputId { get; set; } = "skill-fumble";
|
public string AllowFumbleInputId { get; set; } = "skill-fumble";
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public FormState<SkillFormModel> FormState { get; set; } = new();
|
public SkillFormModel InitialModel { get; set; } = new();
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int FormVersion { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsMutating { get; set; }
|
public bool IsMutating { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback SubmitRequested { get; set; }
|
public Func<SkillFormModel, Task<FormSubmissionResult>> SubmitRequested { get; set; } = _ => Task.FromResult(new FormSubmissionResult());
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback CancelRequested { get; set; }
|
public EventCallback CancelRequested { get; set; }
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
if (!Visible || FormVersion == AppliedFormVersion)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FormState.Model.Name = InitialModel.Name;
|
||||||
|
FormState.Model.DiceRollDefinition = InitialModel.DiceRollDefinition;
|
||||||
|
FormState.Model.WildDice = InitialModel.WildDice;
|
||||||
|
FormState.Model.AllowFumble = InitialModel.AllowFumble;
|
||||||
|
FormState.ResetValidation();
|
||||||
|
AppliedFormVersion = FormVersion;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SubmitAsync()
|
private async Task SubmitAsync()
|
||||||
{
|
{
|
||||||
await SubmitRequested.InvokeAsync();
|
FormState.ResetValidation();
|
||||||
|
|
||||||
|
var result = await SubmitRequested.Invoke(new SkillFormModel
|
||||||
|
{
|
||||||
|
Name = FormState.Model.Name,
|
||||||
|
DiceRollDefinition = FormState.Model.DiceRollDefinition,
|
||||||
|
WildDice = FormState.Model.WildDice,
|
||||||
|
AllowFumble = FormState.Model.AllowFumble
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var (key, value) in result.Errors)
|
||||||
|
{
|
||||||
|
FormState.Errors[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
FormState.ErrorMessage = result.ErrorMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
TECH.md
3
TECH.md
@@ -93,7 +93,8 @@ This pattern is a strong baseline for low to medium scale and should be the defa
|
|||||||
|
|
||||||
- Blazor component tree rooted in `Components/App.razor` and `Components/Pages/Home.razor`.
|
- Blazor component tree rooted in `Components/App.razor` and `Components/Pages/Home.razor`.
|
||||||
- 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 (`Home.Models.cs`) rather than parallel form/error/message property sets.
|
- Form UX state uses reusable `FormState<TModel>` containers in leaf controls (`HomeControls/*`) rather than parallel form/error/message property sets in `Home`.
|
||||||
|
- `Home` workflows return `FormSubmissionResult` so controls can render field and summary errors without parent-owned form wiring.
|
||||||
- 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.
|
||||||
- SSE-driven campaign refresh with reconnect backoff and explicit offline/manual-refresh fallback.
|
- SSE-driven campaign refresh with reconnect backoff and explicit offline/manual-refresh fallback.
|
||||||
|
|||||||
Reference in New Issue
Block a user