diff --git a/README.md b/README.md index d896fc2..d399035 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,14 @@ Follow these steps to set up and run the Blazor Form Builder: Start the Web API project by running the following command script: ```sh - src/run-api.cmd + scripts/run-api.cmd ``` 5. **Run the Blazor Application** Start the Blazor WebAssembly application by running the following command script: ```sh - src/run-app.cmd + scripts/run-app.cmd ``` ### Accessing the Applications diff --git a/src/FormBuilder.API/Controllers/FormController.cs b/src/FormBuilder.API/Controllers/FormController.cs index d131caa..372c32b 100644 --- a/src/FormBuilder.API/Controllers/FormController.cs +++ b/src/FormBuilder.API/Controllers/FormController.cs @@ -60,7 +60,7 @@ public async Task GetFormById(string id) [HttpPost] [ProducesResponseType(typeof(Result
), StatusCodes.Status200OK)] [ProducesResponseType(typeof(Result), StatusCodes.Status400BadRequest)] - public async Task CreateForm([FromBody] CreateFormDto formDto) + public async Task CreateForm(CreateFormDto formDto) { if (string.IsNullOrEmpty(formDto.FormName)) { @@ -91,7 +91,7 @@ public async Task CreateForm([FromBody] CreateFormDto formDto) [HttpPut("{id}")] [ProducesResponseType(typeof(Result), StatusCodes.Status200OK)] [ProducesResponseType(typeof(Result), StatusCodes.Status400BadRequest)] - public async Task UpdateForm(string id, [FromBody] UpdateFormDto formDto) + public async Task UpdateForm(string id, UpdateFormDto formDto) { var existingForm = await _context.Forms.FindAsync(id); @@ -110,7 +110,7 @@ public async Task UpdateForm(string id, [FromBody] UpdateFormDto existingForm.FormDesign = formDto.FormDesign; } - _context.Update(formDto); + _context.Update(existingForm); await _context.SaveChangesAsync(); return Ok(Result.Succeed()); } diff --git a/src/FormBuilder.API/Properties/launchSettings.json b/src/FormBuilder.API/Properties/launchSettings.json index c8c7720..b644c61 100644 --- a/src/FormBuilder.API/Properties/launchSettings.json +++ b/src/FormBuilder.API/Properties/launchSettings.json @@ -4,7 +4,7 @@ "SelfHost": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", "applicationUrl": "https://localhost:7000;http://localhost:5000", "environmentVariables": { diff --git a/src/FormBuilder.DesignerApp/wwwroot/appsettings.json b/src/FormBuilder.DesignerApp/wwwroot/appsettings.json index f2effea..e5e57e0 100644 --- a/src/FormBuilder.DesignerApp/wwwroot/appsettings.json +++ b/src/FormBuilder.DesignerApp/wwwroot/appsettings.json @@ -1,5 +1,5 @@ { "FormBuilderOptions": { - "FormApiUrl": "https://localhost:7000/api" + "FormApiUrl": "https://localhost:7000" } } diff --git a/src/FormBuilder/Components/FormBuilder.razor b/src/FormBuilder/Components/FormBuilder.razor index ecf0e32..7f8dd60 100644 --- a/src/FormBuilder/Components/FormBuilder.razor +++ b/src/FormBuilder/Components/FormBuilder.razor @@ -6,10 +6,10 @@ @if (_formId is not null) { - + } - + @@ -62,10 +62,12 @@ - + Properties +
+
diff --git a/src/FormBuilder/Components/FormBuilder.razor.cs b/src/FormBuilder/Components/FormBuilder.razor.cs index 06eb1a6..373c52a 100644 --- a/src/FormBuilder/Components/FormBuilder.razor.cs +++ b/src/FormBuilder/Components/FormBuilder.razor.cs @@ -24,6 +24,9 @@ public partial class FormBuilder : ComponentBase [Inject] private NotificationService NotificationService { get; set; } = default!; + + [Inject] + private DialogService DialogService { get; set; } = default!; #endregion @@ -32,7 +35,6 @@ private void AddField(FieldType fieldType) { var field = FieldFactory.CreateField(fieldType); field.Label = fieldType.ToString(); - _formDefinition.Fields.Add(field); SelectField(field); } @@ -69,9 +71,9 @@ private void HandleFieldTypeChanged(FieldTypeChangedArgs args) SelectField(newField); } - private void HandleDropField(Action addFieldFn) + private void HandleDropField(Action addFieldCallback) { - addFieldFn(); + addFieldCallback(); } private void SwapFields(Field targetField, Field droppedField) @@ -100,13 +102,28 @@ private async Task SaveFormAsync() if (result.Success) { - NotificationService.Notify(NotificationSeverity.Success, "Form saved successfully"); + NotificationService.NotifySuccess("Form saved successfully"); } else { - NotificationService.Notify(NotificationSeverity.Error, result.Error); + NotificationService.NotifyError(result.Error); } _isLoading = false; } + + private Task OpenLoadFormDialogAsync() + { + return DialogService.OpenAsync("Load Form", new Dictionary + { + { "FormLoaded", EventCallback.Factory.Create(this, LoadForm) } + }); + } + + private void LoadForm(FormCreatedEventArgs args) + { + _formId = args.FormId; + _formDefinition = args.FormDefinition; + StateHasChanged(); + } } diff --git a/src/FormBuilder/Components/LoadFormDialog.razor b/src/FormBuilder/Components/LoadFormDialog.razor new file mode 100644 index 0000000..61cf500 --- /dev/null +++ b/src/FormBuilder/Components/LoadFormDialog.razor @@ -0,0 +1,27 @@ +@using global::FormBuilder.Shared.Models + + + + + Specify either Form ID or JSON design to load into builder + + + + + + + + + + + + + + + + + + + + + diff --git a/src/FormBuilder/Components/LoadFormDialog.razor.cs b/src/FormBuilder/Components/LoadFormDialog.razor.cs new file mode 100644 index 0000000..dab1e4f --- /dev/null +++ b/src/FormBuilder/Components/LoadFormDialog.razor.cs @@ -0,0 +1,96 @@ +using FormBuilder.Models; +using FormBuilder.Services; +using FormBuilder.Shared.Models; +using Microsoft.AspNetCore.Components; +using Radzen; + +namespace FormBuilder.Components; + +public partial class LoadFormDialog : ComponentBase +{ + private FormDto _formModel = new(); + private bool _isLoading; + + #region Injected Servicse + + [Inject] + private NotificationService NotificationService { get; set; } = default!; + + [Inject] + private FormService FormService { get; set; } = default!; + + #endregion + + + #region Parameters + + [Parameter] + public EventCallback FormLoaded { get; set; } + + #endregion + + private bool EitherIdOrDesignRequired() + { + return !string.IsNullOrEmpty(_formModel.Id) || !string.IsNullOrEmpty(_formModel.FormDesign); + } + + private bool ValidateFormDesign() + { + if (string.IsNullOrEmpty(_formModel.FormDesign)) + { + return true; // No form design to validate + } + + var formDefinition = FormService.DeserializeFormDesign(_formModel.FormDesign); + return formDefinition is not null; + } + + private Task LoadFormAsync() + { + if (!string.IsNullOrEmpty(_formModel.Id)) + { + return LoadFormByIdAsync(_formModel.Id); + } + + if (!string.IsNullOrEmpty(_formModel.FormDesign)) + { + return HandleFormDesignDeserialization(_formModel.FormDesign, null); + } + + return Task.CompletedTask; + } + + private async Task LoadFormByIdAsync(string id) + { + _isLoading = true; + _formModel.Id = id; + var result = await FormService.GetFormByIdAsync(_formModel.Id); + + if (result is { Success: true, Data.FormDesign: not null }) + { + await HandleFormDesignDeserialization(result.Data.FormDesign, _formModel.Id); + } + else + { + NotificationService.NotifyError(result.Error!); + } + + _isLoading = false; + } + + private async Task HandleFormDesignDeserialization(string formDesign, string? id) + { + var formDefinition = FormService.DeserializeFormDesign(formDesign); + if (formDefinition != null) + { + await FormLoaded.InvokeAsync(new FormCreatedEventArgs(id, formDefinition)); + NotificationService.NotifySuccess("Form loaded successfully."); + } + else + { + NotificationService.NotifyError("Failed to deserialize form design."); + } + } +} + +public record FormCreatedEventArgs(string? FormId, FormDefinition FormDefinition); diff --git a/src/FormBuilder/Components/PropertyEditor.razor.cs b/src/FormBuilder/Components/PropertyEditor.razor.cs index 1125b88..6f4e79c 100644 --- a/src/FormBuilder/Components/PropertyEditor.razor.cs +++ b/src/FormBuilder/Components/PropertyEditor.razor.cs @@ -76,11 +76,7 @@ private FieldType InputType SelectedField.Type = value; SelectedFieldChanged.InvokeAsync(SelectedField); - FieldTypeChanged.InvokeAsync(new FieldTypeChangedArgs - { - Field = SelectedField, - NewType = value - }); + FieldTypeChanged.InvokeAsync(new FieldTypeChangedArgs(SelectedField, value)); } } @@ -154,15 +150,4 @@ private int? SelectedListValue /// /// Parameters for the FieldTypeChanged event. /// -public class FieldTypeChangedArgs : EventArgs -{ - /// - /// The field that has changed. - /// - public required Field Field { get; set; } - - /// - /// The new type of the field. - /// - public required FieldType NewType { get; set; } -} +public record FieldTypeChangedArgs(Field Field, FieldType NewType); diff --git a/src/FormBuilder/Extensions/NotificationServiceExtensions.cs b/src/FormBuilder/Extensions/NotificationServiceExtensions.cs new file mode 100644 index 0000000..8cc7873 --- /dev/null +++ b/src/FormBuilder/Extensions/NotificationServiceExtensions.cs @@ -0,0 +1,14 @@ +namespace Radzen; + +public static class NotificationServiceExtensions +{ + public static void NotifySuccess(this NotificationService notificationService, string? message) + { + notificationService.Notify(NotificationSeverity.Success, "Success", message); + } + + public static void NotifyError(this NotificationService notificationService, string? message) + { + notificationService.Notify(NotificationSeverity.Error, "Error", message); + } +} diff --git a/src/FormBuilder/Models/Field.cs b/src/FormBuilder/Models/Field.cs index c7c8fd5..b117760 100644 --- a/src/FormBuilder/Models/Field.cs +++ b/src/FormBuilder/Models/Field.cs @@ -1,13 +1,15 @@ -using FormBuilder.Utils; +using System.Text.Json.Serialization; +using FormBuilder.Utils; namespace FormBuilder.Models; /// /// Represents a model for the form field. /// -public abstract class Field +[JsonConverter(typeof(FieldJsonConverter))] +public class Field { - protected Field() + public Field() { if (string.IsNullOrEmpty(Name)) { @@ -55,7 +57,7 @@ protected Field() /// Generic version of the field model with a value of type T. /// /// The type of the field value. -public abstract class Field : Field +public class Field : Field { public T? Value { get; set; } } diff --git a/src/FormBuilder/Services/FormService.cs b/src/FormBuilder/Services/FormService.cs index 42e6f40..3df0aec 100644 --- a/src/FormBuilder/Services/FormService.cs +++ b/src/FormBuilder/Services/FormService.cs @@ -2,12 +2,14 @@ using System.Text.Json; using FormBuilder.Models; using FormBuilder.Shared.Models; +using FormBuilder.Utils; namespace FormBuilder.Services; public class FormService { private readonly HttpClient _httpClient; + private readonly JsonSerializerOptions _jsonSerializerOptions; public FormService(FormBuilderOptions options) { @@ -20,6 +22,39 @@ public FormService(FormBuilderOptions options) { BaseAddress = new Uri(options.FormApiUrl) }; + + _jsonSerializerOptions = new JsonSerializerOptions + { + Converters = { new FieldJsonConverter() } + }; + } + + /// + /// Deserializes the form design JSON string into a FormDefinition object. + /// + /// + /// Serialized form design JSON string from FormDefinition object. + /// + /// + /// FormDefinition object if deserialization is successful, otherwise null. + /// + public FormDefinition? DeserializeFormDesign(string formDesign) + { + try + { + return JsonSerializer.Deserialize(formDesign, _jsonSerializerOptions); + } + catch (JsonException e) + { + Console.WriteLine("Failed to deserialize form design. Error: {0}", e.Message); + return null; + } + } + + public async Task> GetFormByIdAsync(string id) + { + var response = await _httpClient.GetAsync($"/api/forms/{id}"); + return await HandleResponse(response); } public async Task> CreateFormAsync(FormDefinition formDefinition) @@ -31,15 +66,8 @@ public async Task> CreateFormAsync(FormDefinition formDefinition FormDesign = formDesign }; - var response = await _httpClient.PostAsJsonAsync("forms", createFormDto); - var result = await response.Content.ReadFromJsonAsync>(); - - if (result is { Success: true, Data: not null }) - { - return Result.Succeed(result.Data); - } - - return Result.Fail(result!.Error!); + var response = await _httpClient.PostAsJsonAsync("/api/forms", createFormDto); + return await HandleResponse(response); } public async Task UpdateFormAsync(string id, FormDefinition formDefinition) @@ -51,9 +79,30 @@ public async Task UpdateFormAsync(string id, FormDefinition formDefiniti FormDesign = formDesign }; - var response = await _httpClient.PutAsJsonAsync($"forms/{id}", updateFormDto); - var result = await response.Content.ReadFromJsonAsync(); - - return result is { Success: true } ? Result.Succeed() : Result.Fail(result!.Error!); + var response = await _httpClient.PutAsJsonAsync($"/api/forms/{id}", updateFormDto); + return await HandleResponse(response); + } + + private async Task HandleResponse(HttpResponseMessage response) + { + var result = await HandleResponse(response); + return result.Success ? Result.Succeed() : Result.Fail(result.Error!); + } + + private async Task> HandleResponse(HttpResponseMessage response) + { + if (!response.IsSuccessStatusCode) + { + return Result.Fail($"Failed to call API. Status code: {response.StatusCode}, Reason: {response.ReasonPhrase}"); + } + + var result = await response.Content.ReadFromJsonAsync>(); + + if (result is { Success: false, Error: not null }) + { + return Result.Fail(result.Error!); + } + + return Result.Succeed(result!.Data!); } } diff --git a/src/FormBuilder/Utils/FieldJsonConverter.cs b/src/FormBuilder/Utils/FieldJsonConverter.cs new file mode 100644 index 0000000..612d1c6 --- /dev/null +++ b/src/FormBuilder/Utils/FieldJsonConverter.cs @@ -0,0 +1,48 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using FormBuilder.Models; + +namespace FormBuilder.Utils; + +public class FieldJsonConverter : JsonConverter +{ + public override Field Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var jsonDoc = JsonDocument.ParseValue(ref reader); + var rootElement = jsonDoc.RootElement; + + if (!rootElement.TryGetProperty("Type", out var fieldTypeProperty)) + { + throw new JsonException("Field type is missing"); + } + + if (!fieldTypeProperty.TryGetInt32(out var fieldType)) + { + throw new JsonException("Field type is not an integer"); + } + + var enumFieldType = Enum.Parse(fieldType.ToString()); + + Field? field = enumFieldType switch + { + FieldType.Text => JsonSerializer.Deserialize(rootElement.GetRawText(), options), + FieldType.NumericInt => JsonSerializer.Deserialize(rootElement.GetRawText(), options), + FieldType.NumericDouble => JsonSerializer.Deserialize(rootElement.GetRawText(), options), + FieldType.Select => JsonSerializer.Deserialize(rootElement.GetRawText(), options), + FieldType.Date => JsonSerializer.Deserialize(rootElement.GetRawText(), options), + _ => throw new NotSupportedException($"The value of the field type '{enumFieldType}' is not supported"), + }; + + if (field is null) + { + throw new JsonException($"Failed to deserialize field of type {enumFieldType}"); + } + + return field; + } + + public override void Write(Utf8JsonWriter writer, Field value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value.GetType(), options); + } +}