Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions playground/Stress/Stress.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@
var interactionService = commandContext.ServiceProvider.GetRequiredService<IInteractionService>();
var dinnerInput = new InteractionInput
{
InputType = InputType.Select,
InputType = InputType.Choice,
Label = "Dinner",
Placeholder = "Select dinner",
Required = true,
Expand Down Expand Up @@ -231,10 +231,10 @@
var inputs = new List<InteractionInput>
{
new InteractionInput { InputType = InputType.Text, Label = "Name", Placeholder = "Enter name", Required = true },
new InteractionInput { InputType = InputType.Password, Label = "Password", Placeholder = "Enter password", Required = true },
new InteractionInput { InputType = InputType.SecretText, Label = "Password", Placeholder = "Enter password", Required = true },
dinnerInput,
numberOfPeopleInput,
new InteractionInput { InputType = InputType.Checkbox, Label = "Remember me", Placeholder = "What does this do?", Required = true },
new InteractionInput { InputType = InputType.Boolean, Label = "Remember me", Placeholder = "What does this do?", Required = true },
};
var result = await interactionService.PromptInputsAsync(
"Input request",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,41 @@
<p>@((MarkupString)Content.Interaction.Message)</p>
}

<EditForm EditContext="@_editContext">
<EditForm EditContext="@_editContext" OnValidSubmit="@SubmitAsync">
<FluentStack Orientation="Orientation.Vertical" VerticalGap="12">
@foreach (var vm in _inputDialogInputViewModels)
{
@*
* AutoComplete value of one-time-code on password input prevents the browser asking to save the value.
* Immediate value of true on text inputs ensures the value is set to the server token with every key press in textbox.
*@
var localItem = vm;
<div class="interaction-input">
@switch (vm.Input.InputType)
{
case InputType.Text:
<FluentTextField @bind-Value="localItem.Value" Label="@localItem.Input.Label" Placeholder="@localItem.Input.Placeholder" Required="localItem.Input.Required" />
<FluentTextField @ref="@_elementRefs[localItem]"
@bind-Value="localItem.Value"
Label="@localItem.Input.Label"
Placeholder="@localItem.Input.Placeholder"
Required="localItem.Input.Required"
Immediate="true" />
<ValidationMessage For="@(() => localItem.Value)" />
break;
case InputType.Password:
<FluentTextField @bind-Value="localItem.Value" Label="@localItem.Input.Label" Placeholder="@localItem.Input.Placeholder" Required="localItem.Input.Required" TextFieldType="TextFieldType.Password" />
case InputType.SecretText:
<FluentTextField @ref="@_elementRefs[localItem]"
@bind-Value="localItem.Value"
Label="@localItem.Input.Label"
Placeholder="@localItem.Input.Placeholder"
Required="localItem.Input.Required"
TextFieldType="TextFieldType.Password"
AutoComplete="one-time-code"
Immediate="true" />
<ValidationMessage For="@(() => localItem.Value)" />
break;
case InputType.Select:
case InputType.Choice:
<FluentSelect TOption="SelectViewModel<string>"
@ref="@_elementRefs[localItem]"
@bind-Value="localItem.Value"
Label="@localItem.Input.Label"
Placeholder="@localItem.Input.Placeholder"
Expand All @@ -56,11 +73,20 @@
Position="SelectPosition.Below" />
<ValidationMessage For="@(() => localItem.Value)" />
break;
case InputType.Checkbox:
<FluentCheckbox @bind-Value="localItem.IsChecked" Label="@localItem.Input.Label" Placeholder="@localItem.Input.Placeholder" />
case InputType.Boolean:
<FluentCheckbox @ref="@_elementRefs[localItem]"
@bind-Value="localItem.IsChecked"
Label="@localItem.Input.Label"
Placeholder="@localItem.Input.Placeholder" />
break;
case InputType.Number:
<FluentNumberField @bind-Value="localItem.NumberValue" Label="@localItem.Input.Label" Placeholder="@localItem.Input.Placeholder" Required="localItem.Input.Required" />
<FluentNumberField TValue="int?"
@ref="@_elementRefs[localItem]"
@bind-Value="localItem.NumberValue"
Label="@localItem.Input.Label"
Placeholder="@localItem.Input.Placeholder"
Required="localItem.Input.Required"
Immediate="true" />
<ValidationMessage For="@(() => localItem.NumberValue)" />
break;
default:
Expand All @@ -70,11 +96,14 @@
</div>
}
</FluentStack>

@* Hidden submit is so the form is submitted when the user presses enter. *@
<button type="submit" style="display:none"></button>
</EditForm>
</FluentDialogBody>

<FluentDialogFooter>
<FluentButton Appearance="Appearance.Accent" OnClick="@OkAsync">
<FluentButton Appearance="Appearance.Accent" OnClick="@SubmitAsync">
@Dialog.Instance.Parameters.PrimaryAction
</FluentButton>
@if (!string.IsNullOrEmpty(Dialog.Instance.Parameters.SecondaryAction))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public partial class InteractionsInputDialog
private EditContext _editContext = default!;
private ValidationMessageStore _validationMessages = default!;
private List<InputViewModel> _inputDialogInputViewModels = default!;
private Dictionary<InputViewModel, FluentComponentBase?> _elementRefs = default!;

protected override void OnInitialized()
{
Expand All @@ -29,6 +30,8 @@ protected override void OnInitialized()

_editContext.OnValidationRequested += (s, e) => ValidateModel();
_editContext.OnFieldChanged += (s, e) => ValidateField(e.FieldIdentifier);

_elementRefs = new();
}

protected override void OnParametersSet()
Expand All @@ -38,6 +41,15 @@ protected override void OnParametersSet()
_content = Content;
_inputDialogInputViewModels = Content.Inputs.Select(input => new InputViewModel(input)).ToList();

// Initialize keys for @ref binding.
// Do this in case Blazor tries to get the element from the dictionary.
// If the input view model isn't in the dictionary then it will throw a KeyNotFoundException.
_elementRefs.Clear();
foreach (var inputVM in _inputDialogInputViewModels)
{
_elementRefs[inputVM] = null;
}

AddValidationErrorsFromModel();

Content.OnInteractionUpdated = async () =>
Expand All @@ -49,6 +61,31 @@ protected override void OnParametersSet()
}
}

protected override Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Focus the first input when the dialog loads.
if (_inputDialogInputViewModels.Count > 0 && _elementRefs.TryGetValue(_inputDialogInputViewModels[0], out var firstInputElement))
{
if (firstInputElement is FluentInputBase<string> textInput)
{
textInput.FocusAsync();
}
else if (firstInputElement is FluentInputBase<bool> boolInput)
{
boolInput.FocusAsync();
}
else if (firstInputElement is FluentInputBase<int?> numberInput)
{
numberInput.FocusAsync();
}
}
}

return Task.CompletedTask;
}

private void AddValidationErrorsFromModel()
{
for (var i = 0; i < Content.Inputs.Count; i++)
Expand Down Expand Up @@ -101,7 +138,7 @@ private static FieldIdentifier GetFieldIdentifier(InputViewModel inputModel)
{
var fieldName = inputModel.Input.InputType switch
{
InputType.Checkbox => nameof(inputModel.IsChecked),
InputType.Boolean => nameof(inputModel.IsChecked),
InputType.Number => nameof(inputModel.NumberValue),
_ => nameof(inputModel.Value)
};
Expand All @@ -111,11 +148,11 @@ private static FieldIdentifier GetFieldIdentifier(InputViewModel inputModel)
private static bool IsMissingRequiredValue(InputViewModel inputModel)
{
return inputModel.Input.Required &&
inputModel.Input.InputType != InputType.Checkbox &&
inputModel.Input.InputType != InputType.Boolean &&
string.IsNullOrWhiteSpace(inputModel.Value);
}

private async Task OkAsync()
private async Task SubmitAsync()
{
// The workflow is:
// 1. Validate the model that required fields are present.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,8 @@ public async Task<IDialogReference> ShowMessageBoxAsync(IDialogService dialogSer
Width = parameters.Width,
Height = parameters.Height,
AriaLabel = (content.Title ?? ""),
OnDialogResult = parameters.OnDialogResult
OnDialogResult = parameters.OnDialogResult,
PreventDismissOnOverlayClick = true
};
return await dialogService.ShowDialogAsync(typeof(MessageBox), content, dialogParameters);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Model/InputViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public InputViewModel(InteractionInput input)
public void SetInput(InteractionInput input)
{
Input = input;
if (input.InputType == InputType.Select && input.Options != null)
if (input.InputType == InputType.Choice && input.Options != null)
{
var optionsVM = input.Options
.Select(option => new SelectViewModel<string> { Id = option.Key, Name = option.Value, })
Expand Down
14 changes: 7 additions & 7 deletions src/Aspire.Hosting/ApplicationModel/IInteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public sealed class InteractionInput
public bool Required { get; init; }

/// <summary>
/// Gets or sets the options for the input. Only used by <see cref="InputType.Select"/> inputs.
/// Gets or sets the options for the input. Only used by <see cref="InputType.Choice"/> inputs.
/// </summary>
public IReadOnlyList<KeyValuePair<string, string>>? Options { get; init; }

Expand Down Expand Up @@ -151,17 +151,17 @@ public enum InputType
/// </summary>
Text,
/// <summary>
/// A password input.
/// A secure text input.
/// </summary>
Password,
SecretText,
/// <summary>
/// A select input.
/// A choice input. Selects from a list of options.
/// </summary>
Select,
Choice,
/// <summary>
/// A checkbox input.
/// A boolean input.
/// </summary>
Checkbox,
Boolean,
/// <summary>
/// A numeric input.
/// </summary>
Expand Down
6 changes: 3 additions & 3 deletions src/Aspire.Hosting/Dashboard/DashboardService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,9 @@ private static InputType MapInputType(ApplicationModel.InputType inputType)
return inputType switch
{
ApplicationModel.InputType.Text => InputType.Text,
ApplicationModel.InputType.Password => InputType.Password,
ApplicationModel.InputType.Select => InputType.Select,
ApplicationModel.InputType.Checkbox => InputType.Checkbox,
ApplicationModel.InputType.SecretText => InputType.SecretText,
ApplicationModel.InputType.Choice => InputType.Choice,
ApplicationModel.InputType.Boolean => InputType.Boolean,
ApplicationModel.InputType.Number => InputType.Number,
_ => throw new InvalidOperationException($"Unexpected input type: {inputType}"),
};
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/Dashboard/DashboardServiceData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ await _interactionService.CompleteInteractionAsync(
var incomingValue = requestInput.Value;

// Ensure checkbox value is either true or false.
if (requestInput.InputType == Aspire.DashboardService.Proto.V1.InputType.Checkbox)
if (requestInput.InputType == Aspire.DashboardService.Proto.V1.InputType.Boolean)
{
incomingValue = (bool.TryParse(incomingValue, out var b) && b) ? "true" : "false";
}
Expand Down
6 changes: 3 additions & 3 deletions src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -385,9 +385,9 @@ enum MessageIntent {
enum InputType {
INPUT_TYPE_UNSPECIFIED = 0;
INPUT_TYPE_TEXT = 1;
INPUT_TYPE_PASSWORD = 2;
INPUT_TYPE_SELECT = 3;
INPUT_TYPE_CHECKBOX = 4;
INPUT_TYPE_SECRET_TEXT = 2;
INPUT_TYPE_CHOICE = 3;
INPUT_TYPE_BOOLEAN = 4;
INPUT_TYPE_NUMBER = 5;
}

Expand Down
Loading