diff --git a/playground/Stress/Stress.AppHost/Program.cs b/playground/Stress/Stress.AppHost/Program.cs index 065eb163b99..066038fcb6d 100644 --- a/playground/Stress/Stress.AppHost/Program.cs +++ b/playground/Stress/Stress.AppHost/Program.cs @@ -203,7 +203,7 @@ var interactionService = commandContext.ServiceProvider.GetRequiredService(); var dinnerInput = new InteractionInput { - InputType = InputType.Select, + InputType = InputType.Choice, Label = "Dinner", Placeholder = "Select dinner", Required = true, @@ -231,10 +231,10 @@ var inputs = new List { 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", diff --git a/src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor b/src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor index b3fec0ae633..fbb4c76f305 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor +++ b/src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor @@ -27,24 +27,41 @@

@((MarkupString)Content.Interaction.Message)

} - + @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;
@switch (vm.Input.InputType) { case InputType.Text: - + break; - case InputType.Password: - + case InputType.SecretText: + break; - case InputType.Select: + case InputType.Choice: break; - case InputType.Checkbox: - + case InputType.Boolean: + break; case InputType.Number: - + break; default: @@ -70,11 +96,14 @@
}
+ + @* Hidden submit is so the form is submitted when the user presses enter. *@ +
- + @Dialog.Instance.Parameters.PrimaryAction @if (!string.IsNullOrEmpty(Dialog.Instance.Parameters.SecondaryAction)) diff --git a/src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor.cs b/src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor.cs index 343a8628f35..c39d7a975b4 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor.cs +++ b/src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor.cs @@ -21,6 +21,7 @@ public partial class InteractionsInputDialog private EditContext _editContext = default!; private ValidationMessageStore _validationMessages = default!; private List _inputDialogInputViewModels = default!; + private Dictionary _elementRefs = default!; protected override void OnInitialized() { @@ -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() @@ -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 () => @@ -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 textInput) + { + textInput.FocusAsync(); + } + else if (firstInputElement is FluentInputBase boolInput) + { + boolInput.FocusAsync(); + } + else if (firstInputElement is FluentInputBase numberInput) + { + numberInput.FocusAsync(); + } + } + } + + return Task.CompletedTask; + } + private void AddValidationErrorsFromModel() { for (var i = 0; i < Content.Inputs.Count; i++) @@ -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) }; @@ -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. diff --git a/src/Aspire.Dashboard/Components/Interactions/InteractionsProvider.cs b/src/Aspire.Dashboard/Components/Interactions/InteractionsProvider.cs index de7d945613b..f4154584ae3 100644 --- a/src/Aspire.Dashboard/Components/Interactions/InteractionsProvider.cs +++ b/src/Aspire.Dashboard/Components/Interactions/InteractionsProvider.cs @@ -459,7 +459,8 @@ public async Task 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); } diff --git a/src/Aspire.Dashboard/Model/InputViewModel.cs b/src/Aspire.Dashboard/Model/InputViewModel.cs index 43f46752a93..c4720640a6a 100644 --- a/src/Aspire.Dashboard/Model/InputViewModel.cs +++ b/src/Aspire.Dashboard/Model/InputViewModel.cs @@ -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 { Id = option.Key, Name = option.Value, }) diff --git a/src/Aspire.Hosting/ApplicationModel/IInteractionService.cs b/src/Aspire.Hosting/ApplicationModel/IInteractionService.cs index ed3f6efc9bb..a04e8cc174a 100644 --- a/src/Aspire.Hosting/ApplicationModel/IInteractionService.cs +++ b/src/Aspire.Hosting/ApplicationModel/IInteractionService.cs @@ -121,7 +121,7 @@ public sealed class InteractionInput public bool Required { get; init; } /// - /// Gets or sets the options for the input. Only used by inputs. + /// Gets or sets the options for the input. Only used by inputs. /// public IReadOnlyList>? Options { get; init; } @@ -151,17 +151,17 @@ public enum InputType /// Text, /// - /// A password input. + /// A secure text input. /// - Password, + SecretText, /// - /// A select input. + /// A choice input. Selects from a list of options. /// - Select, + Choice, /// - /// A checkbox input. + /// A boolean input. /// - Checkbox, + Boolean, /// /// A numeric input. /// diff --git a/src/Aspire.Hosting/Dashboard/DashboardService.cs b/src/Aspire.Hosting/Dashboard/DashboardService.cs index 83ead31e896..b8f6edcabb5 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardService.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardService.cs @@ -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}"), }; diff --git a/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs b/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs index 3d0a340c279..0bde32de86f 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardServiceData.cs @@ -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"; } diff --git a/src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto b/src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto index 8e77031be25..470d5142837 100644 --- a/src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto +++ b/src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto @@ -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; }