diff --git a/src/Client/Pages/Admin/AdminPage.razor b/src/Client/Pages/Admin/AdminPage.razor
index 3f23c206..ba70de64 100644
--- a/src/Client/Pages/Admin/AdminPage.razor
+++ b/src/Client/Pages/Admin/AdminPage.razor
@@ -4,36 +4,43 @@
Adminsida
-
-
- Hantera kontrakt
- Hantera användare
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ Hantera kontrakt
+ Hantera användare
+
+
+
+
+
+
+
+
+
+
+
+
+
@code {
private string _selectedTab = "contracts";
+ // Null-forgiving operator used because these fields are set by the Blazor runtime using @ref
private ContractTable _contractTable = null!;
private UserTable _userTable = null!;
+ private ContractForm _contractForm = null!;
private void AddContractToTable(Contract contract)
{
- _contractTable.Add(contract);
+ _contractTable.AddOrUpdate(contract);
}
private void AddUserToTable(User user)
@@ -46,4 +53,10 @@
_selectedTab = tabName;
return Task.CompletedTask;
}
+
+ private async Task OnContractOpeningForEdit(Contract contract)
+ {
+ await _contractForm.EditContractAsync(contract);
+ }
+
}
diff --git a/src/Client/Pages/Admin/AdminPage.razor.css b/src/Client/Pages/Admin/AdminPage.razor.css
new file mode 100644
index 00000000..86ba7676
--- /dev/null
+++ b/src/Client/Pages/Admin/AdminPage.razor.css
@@ -0,0 +1,4 @@
+::deep .nav-link {
+ background: #e7e7e7;
+ color: var(--prodigo-dark-blue)
+}
diff --git a/src/Client/Pages/Admin/ContractForm.razor b/src/Client/Pages/Admin/ContractForm.razor
index d67ec07f..7d4d9adc 100644
--- a/src/Client/Pages/Admin/ContractForm.razor
+++ b/src/Client/Pages/Admin/ContractForm.razor
@@ -6,8 +6,9 @@
@using Blazorise.Markdown
@inject ILogger _logger
@inject HttpClient _http
+@inject IJSRuntime _js
-Skapa nytt kontrakt
+
@@ -101,6 +102,38 @@
private const string MinFormHeight = "5rem";
+ ///
+ /// Populates the form with the values of the given .
+ ///
+ /// The data to edit.
+ public async Task EditContractAsync(Contract contract)
+ {
+ await _js.InvokeVoidAsync("scrollToElement", "#contract-form-title");
+ _shouldRender = true;
+ _contract = contract;
+ await DownloadContractFiles();
+ }
+
+ private async Task DownloadContractFiles()
+ {
+ await DownloadContractFile(_contract.InspirationalImagePath, content => _inspirationalContent = content);
+ await DownloadContractFile(_contract.SupplierLogoImagePath, content => _supplierLogoContent = content);
+ await DownloadContractFile(_contract.AdditionalDocument, content => _additionalDocumentContent = content);
+ }
+
+ private async Task DownloadContractFile(string path, Action setContent)
+ {
+
+ var fileUri = new Uri(Path.Join(_http.BaseAddress?.ToString(), path[1..]), UriKind.Absolute);
+
+ HttpResponseMessage response = await _http.GetAsync(fileUri);
+ string mediaType = response.Content.Headers.ContentType?.MediaType ?? "images/jpeg";
+ Stream stream = await response.Content.ReadAsStreamAsync();
+
+ MultipartFormDataContent content = CreateFormDataContent(stream, fileUri.Segments.Last(), mediaType);
+ setContent(content);
+ }
+
private static Contract CreateEmptyContract()
{
return new Contract
@@ -126,34 +159,37 @@
{
_shouldRender = false;
- content = new MultipartFormDataContent();
- StreamContent fileContent;
-
const long bitsInAKilobyte = 1024;
const long kilobytesInAMegabyte = 1024;
const long maxFileSize = bitsInAKilobyte * kilobytesInAMegabyte * 100;
+ Stream fileStream = arg.File.OpenReadStream(maxFileSize);
+
+ content = CreateFormDataContent(fileStream, arg.File.Name, arg.File.ContentType);
+
+ _shouldRender = true;
+ }
+
+ private MultipartFormDataContent CreateFormDataContent(Stream fileStream, string fileName, string contentType)
+ {
+ StreamContent fileContent;
+
try
{
- fileContent = new StreamContent(arg.File.OpenReadStream(maxFileSize));
+ fileContent = new StreamContent(fileStream);
}
catch (IOException ex)
{
- _logger.LogInformation(
- "{FileName} not uploaded: {Message}",
- arg.File.Name, ex.Message);
- return;
+ _logger.LogInformation("{FileName} not uploaded: {Message}", fileName, ex.Message);
+ throw;
}
- fileContent.Headers.ContentType =
- new MediaTypeHeaderValue(arg.File.ContentType);
+ fileContent.Headers.ContentType = new MediaTypeHeaderValue(contentType);
- content.Add(
- fileContent,
- "\"file\"",
- arg.File.Name);
+ var content = new MultipartFormDataContent();
+ content.Add(fileContent, "\"file\"", fileName);
- _shouldRender = true;
+ return content;
}
private async Task OnSubmit()
@@ -192,12 +228,11 @@
{
string json = JsonConvert.SerializeObject(_contract);
var content = new StringContent(json, Encoding.UTF8, "application/json");
- HttpResponseMessage response = await _http.PostAsync("/api/v1/contracts", content);
+ HttpResponseMessage response = await _http.PutAsync($"/api/v1/contracts/{_contract.Id}", content);
if (response.IsSuccessStatusCode)
{
await OnContractUploaded.InvokeAsync(_contract);
_contract = CreateEmptyContract();
}
}
-
}
diff --git a/src/Client/Pages/Admin/ContractTable.razor b/src/Client/Pages/Admin/ContractTable.razor
index f9270292..b349b5a7 100644
--- a/src/Client/Pages/Admin/ContractTable.razor
+++ b/src/Client/Pages/Admin/ContractTable.razor
@@ -12,6 +12,7 @@
Kontraktnamn |
Företag |
+ Redigera |
Ta bort |
@@ -19,7 +20,10 @@
@foreach (Contract contract in contracts)
{
-
+
}
@@ -28,18 +32,35 @@
@code {
- private FetchData> _dataFetcher = null!;
+ ///
+ /// Called when a is being opened to be edited.
+ ///
+ [Parameter]
+ public EventCallback ContractOpeningForEdit { get; set; } = EventCallback.Empty;
///
/// Adds a contract to the list.
///
/// The contract to add.
- public void Add(Contract contract)
+ public void AddOrUpdate(Contract contract)
{
- _dataFetcher.Data?.Add(contract);
+ if (_dataFetcher.Data is null)
+ return;
+
+ if (_dataFetcher.Data.Any(other => other.Id == contract.Id))
+ {
+ _dataFetcher.Data.RemoveAll(toRemove => toRemove.Id == contract.Id);
+ _dataFetcher.Data.Add(contract);
+ }
+ else
+ {
+ _dataFetcher.Data.Add(contract);
+ }
InvokeAsync(StateHasChanged);
}
+ private FetchData> _dataFetcher = null!;
+
///
/// Removes a contract from the list.
///
diff --git a/src/Client/Pages/Admin/ContractTableRow.razor b/src/Client/Pages/Admin/ContractTableRow.razor
index e8dc756a..2ba10805 100644
--- a/src/Client/Pages/Admin/ContractTableRow.razor
+++ b/src/Client/Pages/Admin/ContractTableRow.razor
@@ -7,29 +7,45 @@
@Contract.SupplierName |
+
+
+ |
+
- |
-
+
@code {
+
///
/// Called when a contract has been removed successfully.
///
[Parameter]
public EventCallback OnContractRemoved { get; set; } = EventCallback.Empty;
+ ///
+ /// Called when a is being opened to be edited.
+ ///
+ [Parameter]
+ public EventCallback ContractOpeningForEdit { get; set; } = EventCallback.Empty;
///
/// The contract.
diff --git a/src/Client/wwwroot/css/app.css b/src/Client/wwwroot/css/app.css
index 7149f012..e6510f68 100644
--- a/src/Client/wwwroot/css/app.css
+++ b/src/Client/wwwroot/css/app.css
@@ -23,6 +23,10 @@ p {
line-height: 1.8125rem;
}
+h1, h2, h3, h4, h5, h6 {
+ scroll-margin-top: 4.5rem;
+}
+
h1, h2 {
font-family: 'Merriweather', Helvetica, Arial, sans-serif;
letter-spacing: 0;
@@ -103,12 +107,12 @@ a, .btn-link {
z-index: 1000;
}
- #blazor-error-ui .dismiss {
- cursor: pointer;
- position: absolute;
- right: 0.75rem;
- top: 0.5rem;
- }
+#blazor-error-ui .dismiss {
+ cursor: pointer;
+ position: absolute;
+ right: 0.75rem;
+ top: 0.5rem;
+}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
@@ -116,9 +120,9 @@ a, .btn-link {
color: white;
}
- .blazor-error-boundary::after {
- content: "An error has occurred."
- }
+.blazor-error-boundary::after {
+ content: "An error has occurred."
+}
table {
diff --git a/src/Client/wwwroot/js/interop.js b/src/Client/wwwroot/js/interop.js
index d3c1735c..33faddee 100644
--- a/src/Client/wwwroot/js/interop.js
+++ b/src/Client/wwwroot/js/interop.js
@@ -17,3 +17,7 @@ function registerActivityCallback(dotNetHelper) {
dotNetHelper.invokeMethodAsync("ResetTimer");
}
}
+
+function scrollToElement(selector) {
+ document.querySelector(selector).scrollIntoView({behavior: 'smooth', block: 'start'});
+}
diff --git a/src/Server/Controllers/ContractsController.cs b/src/Server/Controllers/ContractsController.cs
index 5885a6e1..3c4c8cb1 100644
--- a/src/Server/Controllers/ContractsController.cs
+++ b/src/Server/Controllers/ContractsController.cs
@@ -82,22 +82,22 @@ public IActionResult AddRecent(Contract contract)
///
/// Creates a new contract.
///
- /// The contract to add.
+ /// The contract to put.
+ /// The identifier of the contract to put.
/// The identifier of the stored image.
/// The ID of the contract was already taken.
- [HttpPost]
+ [HttpPut("{id:guid}")]
[Authorize("AdminOnly")]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- public IActionResult CreateContract(Contract contract)
+ public IActionResult CreateContract([FromBody] Contract contract, Guid id)
{
try
{
_contracts.Add(contract);
}
- catch (IdentifierAlreadyTakenException e)
+ catch (IdentifierAlreadyTakenException)
{
- Logger.LogInformation("ID of contract was already taken: {Error}", e.Message);
- return BadRequest();
+ _contracts.UpdateContract(contract);
}
return Ok();
diff --git a/tests/Client.Tests/Pages/Admin/ContractListTests.cs b/tests/Client.Tests/Pages/Admin/ContractTableTests.cs
similarity index 74%
rename from tests/Client.Tests/Pages/Admin/ContractListTests.cs
rename to tests/Client.Tests/Pages/Admin/ContractTableTests.cs
index e81047bc..8656476b 100644
--- a/tests/Client.Tests/Pages/Admin/ContractListTests.cs
+++ b/tests/Client.Tests/Pages/Admin/ContractTableTests.cs
@@ -13,7 +13,7 @@
namespace Client.Tests.Pages.Admin;
-public class ContractListTests : UITestFixture
+public class ContractTableTests : UITestFixture
{
[Fact]
public void AddingContract_RendersTheNewContract()
@@ -30,7 +30,7 @@ public void AddingContract_RendersTheNewContract()
var newContract = new Contract { Name = newContractName, };
// Act
- cut.Instance.Add(newContract);
+ cut.Instance.AddOrUpdate(newContract);
cut.WaitForState(() => cut.FindAll(itemSelector).Count == 3);
// Assert
@@ -55,7 +55,7 @@ public void AddingContract_DoesNotThrow_BeforeContractsAreFetched()
var newContract = new Contract { Name = newContractName, };
// Act
- Action add = () => cut.Instance.Add(newContract);
+ Action add = () => cut.Instance.AddOrUpdate(newContract);
// Assert
add.Should().NotThrow();
@@ -68,7 +68,8 @@ public async Task RemovingContract_RendersWithoutTheContractAsync()
var firstContract = new Contract() { Name = "first", };
Contract[] contracts = { firstContract, new Contract() { Name = "Second", }, };
MockHttp.When("/api/v1/contracts").RespondJson(contracts);
- MockHttp.When(HttpMethod.Delete, $"/api/v1/contracts/{firstContract.Id}").Respond(req => new HttpResponseMessage(HttpStatusCode.OK));
+ MockHttp.When(HttpMethod.Delete, $"/api/v1/contracts/{firstContract.Id}")
+ .Respond(req => new HttpResponseMessage(HttpStatusCode.OK));
IRenderedComponent cut = Context.RenderComponent();
const string removeButton = "#confirm-remove";
@@ -83,4 +84,25 @@ public async Task RemovingContract_RendersWithoutTheContractAsync()
elementWithNewName = contract => contract.TextContent.Contains(firstContract.Name);
cut.FindAll(".contract-table-row").Should().NotContain(elementWithNewName);
}
+
+ [Fact]
+ public void UpdatingContract_RendersNewContent_WhenContractExists()
+ {
+ // Arrange
+ var contract = new Contract() { Name = "old name", };
+ Contract[] contracts = { contract, };
+ MockHttp.When("/api/v1/contracts").RespondJson(contracts);
+
+ IRenderedComponent cut = Context.RenderComponent();
+ cut.WaitForAssertion(() => cut.Markup.Should().Contain(contract.Name));
+
+ const string newName = "new name";
+
+ // Act
+ contract.Name = newName;
+ cut.Instance.AddOrUpdate(contract);
+
+ // Assert
+ cut.WaitForAssertion(() => cut.Markup.Should().Contain(newName));
+ }
}
diff --git a/tests/Client.Tests/Pages/Admin/UserListTests.cs b/tests/Client.Tests/Pages/Admin/UserTableTests.cs
similarity index 97%
rename from tests/Client.Tests/Pages/Admin/UserListTests.cs
rename to tests/Client.Tests/Pages/Admin/UserTableTests.cs
index 80ec5850..a990f953 100644
--- a/tests/Client.Tests/Pages/Admin/UserListTests.cs
+++ b/tests/Client.Tests/Pages/Admin/UserTableTests.cs
@@ -13,7 +13,7 @@
namespace Client.Tests.Pages.Admin;
-public class UserListTests : UITestFixture
+public class UserTableTests : UITestFixture
{
[Fact]
public void AddingUser_RendersTheNewUser()
diff --git a/tests/Server.Tests.Integration/Authentication/AuthIntegrationTests.cs b/tests/Server.Tests.Integration/Authentication/AuthIntegrationTests.cs
index 787b5c68..0903f328 100644
--- a/tests/Server.Tests.Integration/Authentication/AuthIntegrationTests.cs
+++ b/tests/Server.Tests.Integration/Authentication/AuthIntegrationTests.cs
@@ -1,35 +1,27 @@
using System.Net;
using System.Net.Http;
-using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Threading.Tasks;
-using Application.Configuration;
+
using Application.Contracts;
-using Application.Users;
+
using Domain.Contracts;
using Domain.Users;
+
using FluentAssertions.Execution;
namespace Server.IntegrationTests.Authentication;
-public class AuthIntegrationTests : IClassFixture
+public class AuthIntegrationTests : IntegrationTest
{
- private readonly HttpClient _client;
-
public AuthIntegrationTests(TestWebApplicationFactory factory)
+ : base(factory)
{
- _client = factory.CreateClient();
-
- Environment.SetEnvironmentVariable(
- ConfigurationKeys.AdminPassword,
- "test_password",
- EnvironmentVariableTarget.Process);
}
public static IEnumerable