Skip to content

Commit e48767c

Browse files
committed
Make drag and drop available
1 parent 111b0bf commit e48767c

File tree

13 files changed

+142
-30
lines changed

13 files changed

+142
-30
lines changed

LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/ProfileRepositoryTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ public async Task ShouldSaveAndRetrieveAllEntries()
1212
{
1313
var item1 = new ProfileInformationEntryBuilder().WithContent("key1").Build();
1414
var item2 = new ProfileInformationEntryBuilder().WithContent("key2").Build();
15-
await ProfileRepository.AddAsync(item1);
16-
await ProfileRepository.AddAsync(item2);
15+
await ProfileRepository.StoreAsync(item1);
16+
await ProfileRepository.StoreAsync(item2);
1717

1818
var items = await ProfileRepository.GetAllAsync();
1919

@@ -26,8 +26,8 @@ public async Task ShouldDelete()
2626
{
2727
var item1 = new ProfileInformationEntryBuilder().WithContent("key1").Build();
2828
var item2 = new ProfileInformationEntryBuilder().WithContent("key2").Build();
29-
await ProfileRepository.AddAsync(item1);
30-
await ProfileRepository.AddAsync(item2);
29+
await ProfileRepository.StoreAsync(item1);
30+
await ProfileRepository.StoreAsync(item2);
3131

3232
await ProfileRepository.DeleteAsync(item1.Id);
3333

@@ -40,7 +40,7 @@ public async Task ShouldDelete()
4040
public async Task NoopOnDeleteWhenEntryNotFound()
4141
{
4242
var item = new ProfileInformationEntryBuilder().WithContent("key1").Build();
43-
await ProfileRepository.AddAsync(item);
43+
await ProfileRepository.StoreAsync(item);
4444

4545
await ProfileRepository.DeleteAsync("SomeIdWhichHopefullyDoesNotExist");
4646

LinkDotNet.Blog.IntegrationTests/Web/Shared/ProfileTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public void ShouldAddEntry()
5151
{
5252
var repo = RegisterServices();
5353
ProfileInformationEntry entryToDb = null;
54-
repo.Setup(p => p.AddAsync(It.IsAny<ProfileInformationEntry>()))
54+
repo.Setup(p => p.StoreAsync(It.IsAny<ProfileInformationEntry>()))
5555
.Callback<ProfileInformationEntry>(p => entryToDb = p);
5656
var cut = RenderComponent<Profile>(p => p.Add(s => s.IsAuthenticated, true));
5757
var addShortItemComponent = cut.FindComponent<AddProfileShortItem>();
@@ -61,7 +61,7 @@ public void ShouldAddEntry()
6161

6262
entryToDb.Should().NotBeNull();
6363
entryToDb.Content.Should().Be("key");
64-
entryToDb.SortOrder.Should().Be(0);
64+
entryToDb.SortOrder.Should().Be(1000);
6565
}
6666

6767
[Fact]
@@ -101,7 +101,7 @@ public void ShouldAddEntryWithCorrectSortOrder()
101101
var entry = new ProfileInformationEntryBuilder().WithSortOrder(1).Build();
102102
repo.Setup(p => p.GetAllAsync()).ReturnsAsync(new[] { entry });
103103
ProfileInformationEntry entryToDb = null;
104-
repo.Setup(p => p.AddAsync(It.IsAny<ProfileInformationEntry>()))
104+
repo.Setup(p => p.StoreAsync(It.IsAny<ProfileInformationEntry>()))
105105
.Callback<ProfileInformationEntry>(p => entryToDb = p);
106106
var cut = RenderComponent<Profile>(p => p.Add(s => s.IsAuthenticated, true));
107107
var addShortItemComponent = cut.FindComponent<AddProfileShortItem>();

LinkDotNet.Blog.UnitTests/Infrastructure/Persistence/InMemory/ProfileRepositoryTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ public async Task ShouldSaveAndRetrieveAllEntries()
2020
{
2121
var item1 = new ProfileInformationEntryBuilder().WithContent("key1").Build();
2222
var item2 = new ProfileInformationEntryBuilder().WithContent("key2").Build();
23-
await profileRepository.AddAsync(item1);
24-
await profileRepository.AddAsync(item2);
23+
await profileRepository.StoreAsync(item1);
24+
await profileRepository.StoreAsync(item2);
2525

2626
var items = await profileRepository.GetAllAsync();
2727

@@ -34,8 +34,8 @@ public async Task ShouldDelete()
3434
{
3535
var item1 = new ProfileInformationEntryBuilder().WithContent("key1").Build();
3636
var item2 = new ProfileInformationEntryBuilder().WithContent("key2").Build();
37-
await profileRepository.AddAsync(item1);
38-
await profileRepository.AddAsync(item2);
37+
await profileRepository.StoreAsync(item1);
38+
await profileRepository.StoreAsync(item2);
3939

4040
await profileRepository.DeleteAsync(item1.Id);
4141

@@ -48,7 +48,7 @@ public async Task ShouldDelete()
4848
public async Task NoopOnDeleteWhenEntryNotFound()
4949
{
5050
var item = new ProfileInformationEntryBuilder().WithContent("key1").Build();
51-
await profileRepository.AddAsync(item);
51+
await profileRepository.StoreAsync(item);
5252

5353
await profileRepository.DeleteAsync("SomeIdWhichHopefullyDoesNotExist");
5454

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using FluentAssertions;
2+
using LinkDotNet.Blog.TestUtilities;
3+
using LinkDotNet.Blog.Web.Shared;
4+
using Xunit;
5+
6+
namespace LinkDotNet.Blog.UnitTests.Web.Shared
7+
{
8+
public class SortOrderCalculatorTests
9+
{
10+
private SortOrderCalculator sut;
11+
12+
public SortOrderCalculatorTests()
13+
{
14+
sut = new SortOrderCalculator();
15+
}
16+
17+
[Fact]
18+
public void ShouldProperlyCalculateNewSortOrder()
19+
{
20+
var target = new ProfileInformationEntryBuilder().WithSortOrder(1000).Build();
21+
var next = new ProfileInformationEntryBuilder().WithSortOrder(2000).Build();
22+
var all = new[] { target, next };
23+
24+
var newSortOrder = sut.GetSortOrder(target, all);
25+
26+
newSortOrder.Should().Be(1500);
27+
}
28+
29+
[Fact]
30+
public void ShouldProperlyCalculateNewSortOrderWhenLastItem()
31+
{
32+
var target = new ProfileInformationEntryBuilder().WithSortOrder(2000).Build();
33+
var previous = new ProfileInformationEntryBuilder().WithSortOrder(1000).Build();
34+
var all = new[] { previous, target };
35+
36+
var newSortOrder = sut.GetSortOrder(target, all);
37+
38+
newSortOrder.Should().Be(1500);
39+
}
40+
}
41+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Collections.Generic;
2+
using LinkDotNet.Domain;
3+
4+
namespace LinkDotNet.Blog.Web.Shared
5+
{
6+
public interface ISortOrderCalculator
7+
{
8+
int GetSortOrder(ProfileInformationEntry target, IEnumerable<ProfileInformationEntry> all);
9+
}
10+
}

LinkDotNet.Blog.Web/Shared/Profile.razor

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
@inject AppConfiguration appConfiguration
66
@inject IProfileRepository repository
7+
@inject ISortOrderCalculator sortOrderCalculator
78
<div class="profile">
89
<div class="profile-name">
910
<span>@appConfiguration.ProfileInformation.Name</span>
@@ -13,19 +14,27 @@
1314
<div class="profile-image">
1415
<img src="@appConfiguration.ProfileInformation.ProfilePictureUrl" alt="Profile Picture" />
1516
</div>
16-
<ul class="profile-keypoints inverted-colors">
17+
<ul class="profile-keypoints inverted-colors" ondragover="event.preventDefault();">
1718
@foreach (var entry in profileInformationEntries)
1819
{
19-
<li>
20-
@if (IsAuthenticated)
21-
{
22-
<button type="button" class="btn btn-default" aria-label="Delete Item" @onclick="() =>
23-
ShowDeleteDialog(entry.Content)">
20+
@if (IsAuthenticated)
21+
{
22+
<li
23+
style="cursor: move"
24+
draggable="true"
25+
@ondrag="@(() => currentDragItem = entry)"
26+
@ondrop="@(() => HandleDrop(entry))">
27+
28+
<button type="button" class="btn btn-default" aria-label="Delete Item" @onclick="() => ShowDeleteDialog(entry.Content)">
2429
<i class="fas fa-trash-alt" aria-hidden="true"></i>
2530
</button>
26-
}
27-
@RenderMarkupString(entry.Content)
28-
</li>
31+
@RenderMarkupString(entry.Content)
32+
</li>
33+
}
34+
else
35+
{
36+
<li>@RenderMarkupString(entry.Content)</li>
37+
}
2938
}
3039
@if (IsAuthenticated)
3140
{
@@ -41,9 +50,10 @@ OnYesPressed="DeleteItem"></ConfirmDialog>
4150
[Parameter]
4251
public bool IsAuthenticated { get; set; }
4352

44-
private IList<ProfileInformationEntry> profileInformationEntries = new List<ProfileInformationEntry>();
53+
private List<ProfileInformationEntry> profileInformationEntries = new();
4554
private ConfirmDialog Dialog { get; set; }
4655
private string currentDeleteKey;
56+
private ProfileInformationEntry currentDragItem;
4757

4858
protected override async Task OnInitializedAsync()
4959
{
@@ -69,7 +79,7 @@ OnYesPressed="DeleteItem"></ConfirmDialog>
6979
var newEntry = ProfileInformationEntry.Create(toAdd, sortOrder);
7080

7181
profileInformationEntries.Add(newEntry);
72-
await repository.AddAsync(newEntry);
82+
await repository.StoreAsync(newEntry);
7383
}
7484

7585
private int GetSortOrder()
@@ -79,6 +89,22 @@ OnYesPressed="DeleteItem"></ConfirmDialog>
7989
return profileInformationEntries.Max(p => p.SortOrder) + 1000;
8090
}
8191

82-
return 0;
92+
return 1000;
93+
}
94+
95+
private async Task HandleDrop(ProfileInformationEntry dropTarget)
96+
{
97+
if (currentDragItem == null || dropTarget == currentDragItem)
98+
{
99+
return;
100+
}
101+
102+
var newSortOrder = sortOrderCalculator.GetSortOrder(dropTarget, profileInformationEntries);
103+
currentDragItem.SortOrder = newSortOrder;
104+
await repository.StoreAsync(currentDragItem);
105+
currentDragItem = null;
106+
profileInformationEntries.Sort((a, b) => a.SortOrder.CompareTo(b.SortOrder));
107+
StateHasChanged();
83108
}
109+
84110
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Collections.Generic;
2+
using LinkDotNet.Domain;
3+
4+
namespace LinkDotNet.Blog.Web.Shared
5+
{
6+
public class SortOrderCalculator : ISortOrderCalculator
7+
{
8+
public int GetSortOrder(ProfileInformationEntry target, IEnumerable<ProfileInformationEntry> all)
9+
{
10+
var linkedEntries = new LinkedList<ProfileInformationEntry>(all);
11+
var targetNode = linkedEntries.Find(target);
12+
var next = targetNode!.Next;
13+
14+
if (next == null)
15+
{
16+
var prev = targetNode.Previous;
17+
return (target.SortOrder + prev!.Value.SortOrder) / 2;
18+
}
19+
20+
return (target.SortOrder + next.Value.SortOrder) / 2;
21+
}
22+
}
23+
}

LinkDotNet.Blog.Web/Startup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using LinkDotNet.Blog.Web.Authentication.Auth0;
44
using LinkDotNet.Blog.Web.Authentication.Dummy;
55
using LinkDotNet.Blog.Web.RegistrationExtensions;
6+
using LinkDotNet.Blog.Web.Shared;
67
using Microsoft.AspNetCore.Builder;
78
using Microsoft.AspNetCore.Hosting;
89
using Microsoft.Extensions.Configuration;
@@ -60,6 +61,7 @@ public void ConfigureServices(IServiceCollection services)
6061
services.AddBlazoredToast();
6162
services.AddBlazoredLocalStorage();
6263
services.AddHeadElementHelper();
64+
services.AddSingleton<ISortOrderCalculator, SortOrderCalculator>();
6365
}
6466

6567
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

LinkDotNet.Domain/ProfileInformationEntry.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
2+
using System.Diagnostics;
23

34
namespace LinkDotNet.Domain
45
{
6+
[DebuggerDisplay("{Content}")]
57
public class ProfileInformationEntry
68
{
79
private ProfileInformationEntry()

LinkDotNet.Infrastructure/Persistence/IProfileRepository.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public interface IProfileRepository
88
{
99
Task<IList<ProfileInformationEntry>> GetAllAsync();
1010

11-
Task AddAsync(ProfileInformationEntry entry);
11+
Task StoreAsync(ProfileInformationEntry entry);
1212

1313
Task DeleteAsync(string id);
1414
}

0 commit comments

Comments
 (0)