From 90adbcfb7c9d415306f911dd904fe9f95227c57b Mon Sep 17 00:00:00 2001 From: Neil Brommer Date: Tue, 19 Apr 2022 13:04:38 -0700 Subject: [PATCH] Add sorting columns --- Readme.md | 4 +- Start/Client/Components/CreateBookmark.razor | 4 +- Start/Client/Components/CreateContainer.razor | 2 +- Start/Client/Components/CreateGroup.razor | 4 +- .../ContainersList/ContainerListReducers.cs | 10 +- .../CreateBookmark/CreateBookmarkEffects.cs | 24 +- .../CreateContainer/CreateContainerEffects.cs | 18 +- .../CreateGroup/CreateGroupEffects.cs | 22 +- .../CurrentContainerEffects.cs | 2 +- .../CurrentContainerReducers.cs | 20 +- .../BookmarkContainersController.cs | 4 +- .../Controllers/BookmarkGroupsController.cs | 4 +- .../Server/Controllers/BookmarksController.cs | 5 +- .../20211220002043_AddSorting.Designer.cs | 508 ++++++++++++++++++ .../Migrations/20211220002043_AddSorting.cs | 42 ++ .../ApplicationDbContextModelSnapshot.cs | 10 + .../Data/Services/BookmarkContainerService.cs | 70 ++- .../Data/Services/BookmarkGroupService.cs | 4 +- Start/Server/Data/Services/BookmarkService.cs | 6 +- .../Interfaces/IBookmarkContainerService.cs | 2 +- .../Interfaces/IBookmarkGroupService.cs | 6 +- .../Services/Interfaces/IBookmarkService.cs | 2 +- Start/Server/Extensions/BookmarkMaps.cs | 6 +- Start/Server/Extensions/SortingExtensions.cs | 28 + Start/Server/Models/Bookmark.cs | 12 +- Start/Server/Models/BookmarkContainer.cs | 14 +- Start/Server/Models/BookmarkGroup.cs | 12 +- Start/Shared/Api/IBookmarkContainersApi.cs | 4 +- Start/Shared/Api/IBookmarkGroupsApi.cs | 2 +- Start/Shared/Api/IBookmarksApi.cs | 2 +- Start/Shared/BookmarkContainerDto.cs | 11 +- Start/Shared/BookmarkDto.cs | 9 +- Start/Shared/BookmarkGroupDto.cs | 13 +- Start/Shared/SortingExtensions.cs | 27 + 34 files changed, 833 insertions(+), 80 deletions(-) create mode 100644 Start/Server/Data/Migrations/20211220002043_AddSorting.Designer.cs create mode 100644 Start/Server/Data/Migrations/20211220002043_AddSorting.cs create mode 100644 Start/Server/Extensions/SortingExtensions.cs create mode 100644 Start/Shared/SortingExtensions.cs diff --git a/Readme.md b/Readme.md index e3d3831..4b48c9a 100644 --- a/Readme.md +++ b/Readme.md @@ -25,8 +25,8 @@ This is a rewrite of my [New Tab Page project](https://github.com/NeilBrommer/Ne - [x] Delete - [ ] Edit - [ ] Manage bookmarks - - [ ] Create - - [ ] Delete + - [x] Create + - [x] Delete - [ ] Edit - [x] Use [Refit](https://github.com/reactiveui/refit) for strongly typed API calls - [ ] Support choosing between storing data on the server or in IndexedDB diff --git a/Start/Client/Components/CreateBookmark.razor b/Start/Client/Components/CreateBookmark.razor index fad12ac..3ddb6af 100644 --- a/Start/Client/Components/CreateBookmark.razor +++ b/Start/Client/Components/CreateBookmark.razor @@ -73,13 +73,13 @@ @code { - protected BookmarkDto Model { get; set; } = new BookmarkDto("", "", null, 0); + protected BookmarkDto Model { get; set; } = new BookmarkDto("", "", null, 0, 0); protected override void OnInitialized() { base.OnInitialized(); - this.Model = new BookmarkDto("", "", null, this.state.Value.GroupId); + this.Model = new BookmarkDto("", "", null, 0, this.state.Value.GroupId); actionSubscriber.SubscribeToAction(this, a => this.Model.BookmarkGroupId = a.GroupId); diff --git a/Start/Client/Components/CreateContainer.razor b/Start/Client/Components/CreateContainer.razor index f21abbf..ef18970 100644 --- a/Start/Client/Components/CreateContainer.razor +++ b/Start/Client/Components/CreateContainer.razor @@ -65,7 +65,7 @@ [Parameter] public EventCallback OnCreated { get; set; } - private BookmarkContainerDto Model { get; set; } = new BookmarkContainerDto(""); + private BookmarkContainerDto Model { get; set; } = new BookmarkContainerDto("", 0); protected void OnSubmit() { diff --git a/Start/Client/Components/CreateGroup.razor b/Start/Client/Components/CreateGroup.razor index 238942a..28bf4f6 100644 --- a/Start/Client/Components/CreateGroup.razor +++ b/Start/Client/Components/CreateGroup.razor @@ -63,13 +63,13 @@ @code { - private BookmarkGroupDto Model { get; set; } = new("", "", 0); + private BookmarkGroupDto Model { get; set; } = new("", "", 0, 0); protected override void OnInitialized() { base.OnInitialized(); - this.Model = new BookmarkGroupDto("", "", state.Value.ContainerId); + this.Model = new BookmarkGroupDto("", "", 0, state.Value.ContainerId); // Keep the model's container ID up to date actionSubscriber.SubscribeToAction(this, diff --git a/Start/Client/Store/Features/ContainersList/ContainerListReducers.cs b/Start/Client/Store/Features/ContainersList/ContainerListReducers.cs index c5d8459..5bef518 100644 --- a/Start/Client/Store/Features/ContainersList/ContainerListReducers.cs +++ b/Start/Client/Store/Features/ContainersList/ContainerListReducers.cs @@ -21,7 +21,9 @@ namespace Start.Client.Store.Features.ContainersList { RecievedContainerListAction action) { return state with { ContainerListState = state.ContainerListState with { - Containers = action.Containers.ToImmutableList(), + Containers = action.Containers + .SortContainers() + .ToImmutableList(), IsLoadingContainersList = false, ErrorMessage = null } @@ -43,7 +45,10 @@ namespace Start.Client.Store.Features.ContainersList { AddContainerToListAction action) { return state with { ContainerListState = state.ContainerListState with { - Containers = state.ContainerListState.Containers.Add(action.NewContainer) + Containers = state.ContainerListState.Containers + .Add(action.NewContainer) + .SortContainers() + .ToImmutableList() } }; } @@ -55,6 +60,7 @@ namespace Start.Client.Store.Features.ContainersList { ContainerListState = state.ContainerListState with { Containers = state.ContainerListState.Containers .Where(c => c.BookmarkContainerId != action.ContainerIdToRemove) + .SortContainers() .ToImmutableList() } }; diff --git a/Start/Client/Store/Features/CreateBookmark/CreateBookmarkEffects.cs b/Start/Client/Store/Features/CreateBookmark/CreateBookmarkEffects.cs index b0436a8..c8f8633 100644 --- a/Start/Client/Store/Features/CreateBookmark/CreateBookmarkEffects.cs +++ b/Start/Client/Store/Features/CreateBookmark/CreateBookmarkEffects.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using System.Linq; using Fluxor; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Start.Shared.Api; @@ -7,9 +8,12 @@ using Start.Client.Store.Features.CurrentContainer; namespace Start.Client.Store.Features.CreateBookmark { public class CreateBookmarkEffects { public IBookmarksApi BookmarksApi { get; init; } + public IBookmarkGroupsApi BookmarkGroupsApi { get; init; } - public CreateBookmarkEffects(IBookmarksApi bookmarksApi) { + public CreateBookmarkEffects(IBookmarksApi bookmarksApi, + IBookmarkGroupsApi bookmarkGroupsApi) { this.BookmarksApi = bookmarksApi; + this.BookmarkGroupsApi = bookmarkGroupsApi; } [EffectMethod] @@ -18,9 +22,25 @@ namespace Start.Client.Store.Features.CreateBookmark { dispatch.Dispatch(new FetchCreateBookmarkAction()); try { + Refit.ApiResponse? groupResponse = await this + .BookmarkGroupsApi + .GetBookmarkGroup(action.NewBookmark.BookmarkGroupId); + + if (groupResponse == null || groupResponse.Content == null) { + dispatch.Dispatch(new ErrorFetchingCreateBookmarkAction( + "There was an error checking the bookmark group")); + return; + } + + // Set the sort order to highest in the group + 1 + // .Max throws an exception if Bookmarks is empty + int sortOrder = !(groupResponse.Content.Bookmarks?.Any() ?? false) + ? 0 + : groupResponse.Content.Bookmarks.Max(b => b.SortOrder) + 1; + Refit.ApiResponse? apiResponse = await this.BookmarksApi .CreateBookmark(action.NewBookmark.Title, action.NewBookmark.Url, - action.NewBookmark.Notes, action.NewBookmark.BookmarkGroupId); + action.NewBookmark.Notes, sortOrder, action.NewBookmark.BookmarkGroupId); if (!apiResponse.IsSuccessStatusCode) { dispatch.Dispatch(new ErrorFetchingCreateBookmarkAction( diff --git a/Start/Client/Store/Features/CreateContainer/CreateContainerEffects.cs b/Start/Client/Store/Features/CreateContainer/CreateContainerEffects.cs index 5fd710b..8a781f6 100644 --- a/Start/Client/Store/Features/CreateContainer/CreateContainerEffects.cs +++ b/Start/Client/Store/Features/CreateContainer/CreateContainerEffects.cs @@ -5,6 +5,8 @@ using Refit; using Start.Shared; using Start.Shared.Api; using Start.Client.Store.Features.ContainersList; +using System.Collections.Generic; +using System.Linq; namespace Start.Client.Store.Features.CreateContainer { public class CreateContainerEffects { @@ -20,8 +22,22 @@ namespace Start.Client.Store.Features.CreateContainer { dispatch.Dispatch(new FetchCreateContainerAction()); try { + ApiResponse>? containersResponse = await this + .BookmarkContainersApi + .GetAllBookmarkContainers(); + + if (containersResponse == null || containersResponse.Content == null) { + dispatch.Dispatch(new ErrorFetchingCreateContainerAction( + "There was an error checking bookmark containers")); + return; + } + + int sortOrder = !containersResponse.Content.Any() + ? 0 + : containersResponse.Content.Max(c => c.SortOrder) + 1; + ApiResponse apiResponse = await this.BookmarkContainersApi - .CreateBookmarkContainer(action.NewContainer.Title); + .CreateBookmarkContainer(action.NewContainer.Title, sortOrder); BookmarkContainerDto? container = apiResponse.Content; diff --git a/Start/Client/Store/Features/CreateGroup/CreateGroupEffects.cs b/Start/Client/Store/Features/CreateGroup/CreateGroupEffects.cs index d5329b7..34d0a64 100644 --- a/Start/Client/Store/Features/CreateGroup/CreateGroupEffects.cs +++ b/Start/Client/Store/Features/CreateGroup/CreateGroupEffects.cs @@ -6,13 +6,17 @@ using Refit; using Start.Shared; using Start.Client.Store.Features.CurrentContainer; using System; +using System.Linq; namespace Start.Client.Store.Features.CreateGroup { public class CreateGroupEffects { public IBookmarkGroupsApi BookmarkGroupsApi { get; init; } + public IBookmarkContainersApi BookmarkContainersApi { get; init; } - public CreateGroupEffects(IBookmarkGroupsApi bookmarksApi) { + public CreateGroupEffects(IBookmarkGroupsApi bookmarksApi, + IBookmarkContainersApi bookmarkContainersApi) { this.BookmarkGroupsApi = bookmarksApi; + this.BookmarkContainersApi = bookmarkContainersApi; } [EffectMethod] @@ -21,8 +25,22 @@ namespace Start.Client.Store.Features.CreateGroup { dispatch.Dispatch(new FetchCreateGroupAction()); try { + ApiResponse? containerResponse = await this + .BookmarkContainersApi + .GetBookmarkContainer(action.NewGroup.BookmarkContainerId); + + if (containerResponse == null || containerResponse.Content == null) { + dispatch.Dispatch(new ErrorFetchingCreateGroupAction( + "There was an error checking the new group's bookmark container")); + return; + } + + int sortOrder = !(containerResponse.Content.BookmarkGroups?.Any() ?? false) + ? 0 + : containerResponse.Content.BookmarkGroups.Max(g => g.SortOrder) + 1; + ApiResponse apiResponse = await this.BookmarkGroupsApi - .CreateBookmarkGroup(action.NewGroup.Title, action.NewGroup.Color, + .CreateBookmarkGroup(action.NewGroup.Title, action.NewGroup.Color, sortOrder, action.NewGroup.BookmarkContainerId); Console.WriteLine("Status code: " + apiResponse.StatusCode); diff --git a/Start/Client/Store/Features/CurrentContainer/CurrentContainerEffects.cs b/Start/Client/Store/Features/CurrentContainer/CurrentContainerEffects.cs index 342bcbc..22faf6c 100644 --- a/Start/Client/Store/Features/CurrentContainer/CurrentContainerEffects.cs +++ b/Start/Client/Store/Features/CurrentContainer/CurrentContainerEffects.cs @@ -62,7 +62,7 @@ namespace Start.Client.Store.Features.CurrentContainer { #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously if (!this.RootState.Value.ContainerListState.Containers.Any()) { dispatch.Dispatch(new SubmitCreateContainerAction( - new BookmarkContainerDto("Default"))); + new BookmarkContainerDto("Default", 0))); return; } diff --git a/Start/Client/Store/Features/CurrentContainer/CurrentContainerReducers.cs b/Start/Client/Store/Features/CurrentContainer/CurrentContainerReducers.cs index 23b05c5..c6cddc2 100644 --- a/Start/Client/Store/Features/CurrentContainer/CurrentContainerReducers.cs +++ b/Start/Client/Store/Features/CurrentContainer/CurrentContainerReducers.cs @@ -20,6 +20,11 @@ namespace Start.Client.Store.Features.CurrentContainer { [ReducerMethod] public static RootState ReceivedCurrentContainer(RootState state, ReceivedCurrentContainerAction action) { + BookmarkContainerDto? container = action.BookmarkContainer; + container.BookmarkGroups = container.BookmarkGroups + ?.SortGroups() + .ToList(); + return state with { CurrentContainerState = state.CurrentContainerState with { Container = action.BookmarkContainer, @@ -54,8 +59,9 @@ namespace Start.Client.Store.Features.CurrentContainer { return state with { CurrentContainerState = state.CurrentContainerState with { Container = new BookmarkContainerDto(container.BookmarkContainerId, - container.Title, container.BookmarkGroups? + container.Title, container.SortOrder, container.BookmarkGroups? .Concat(new List { action.BookmarkGroup }) + .SortGroups() .ToList()) } }; @@ -72,7 +78,7 @@ namespace Start.Client.Store.Features.CurrentContainer { return state with { CurrentContainerState = state.CurrentContainerState with { Container = new BookmarkContainerDto(container.BookmarkContainerId, - container.Title, container.BookmarkGroups? + container.Title, container.SortOrder, container.BookmarkGroups? .Where(g => g.BookmarkGroupId != action.BookmarkGroupId) .ToList()) } @@ -90,9 +96,9 @@ namespace Start.Client.Store.Features.CurrentContainer { ?.Select(bg => { if (bg.BookmarkGroupId == action.Bookmark.BookmarkGroupId) { return new BookmarkGroupDto(bg.BookmarkGroupId, bg.Title, bg.Color, - bg.BookmarkContainerId, - bg.Bookmarks? + bg.SortOrder, bg.BookmarkContainerId, bg.Bookmarks? .Concat(new List { action.Bookmark }) + .SortBookmarks() .ToList()); } @@ -103,7 +109,7 @@ namespace Start.Client.Store.Features.CurrentContainer { return state with { CurrentContainerState = state.CurrentContainerState with { Container = new BookmarkContainerDto(container.BookmarkContainerId, - container.Title, groups) + container.Title, container.SortOrder, groups) } }; } @@ -117,7 +123,7 @@ namespace Start.Client.Store.Features.CurrentContainer { List? groups = container.BookmarkGroups ?.Select(bg => new BookmarkGroupDto(bg.BookmarkGroupId, bg.Title, bg.Color, - bg.BookmarkContainerId, bg.Bookmarks + bg.SortOrder, bg.BookmarkContainerId, bg.Bookmarks ?.Where(b => b.BookmarkId != action.BookmarkId) .ToList())) .ToList(); @@ -125,7 +131,7 @@ namespace Start.Client.Store.Features.CurrentContainer { return state with { CurrentContainerState = state.CurrentContainerState with { Container = new BookmarkContainerDto(container.BookmarkContainerId, - container.Title, groups) + container.Title, container.SortOrder, groups) } }; } diff --git a/Start/Server/Controllers/BookmarkContainersController.cs b/Start/Server/Controllers/BookmarkContainersController.cs index 951c017..c722afc 100644 --- a/Start/Server/Controllers/BookmarkContainersController.cs +++ b/Start/Server/Controllers/BookmarkContainersController.cs @@ -54,9 +54,9 @@ namespace Start.Server.Controllers { [Route("Create")] [ProducesResponseType(StatusCodes.Status201Created, Type = typeof(BookmarkContainerDto))] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task CreateBookmarkContainer([FromBody] string title) { + public async Task CreateBookmarkContainer(string title, int sortOrder) { BookmarkContainerDto? container = (await this.bookmarkContainerService - .CreateBookmarkContainer(this.GetAuthorizedUserId(), title)) + .CreateBookmarkContainer(this.GetAuthorizedUserId(), title, sortOrder)) ?.MapToDto(); if (container == null) diff --git a/Start/Server/Controllers/BookmarkGroupsController.cs b/Start/Server/Controllers/BookmarkGroupsController.cs index db733d7..dff7bb2 100644 --- a/Start/Server/Controllers/BookmarkGroupsController.cs +++ b/Start/Server/Controllers/BookmarkGroupsController.cs @@ -37,9 +37,9 @@ namespace Start.Server.Controllers { [ProducesResponseType(StatusCodes.Status201Created, Type = typeof(BookmarkGroupDto))] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task CreateBookmarkGroup(string title, string color, - int bookmarkContainerId) { + int sortOrder, int bookmarkContainerId) { BookmarkGroup? newGroup = await this.bookmarkGroupService - .CreateBookmarkGroup(this.GetAuthorizedUserId(), title, color, bookmarkContainerId); + .CreateBookmarkGroup(this.GetAuthorizedUserId(), title, color, sortOrder, bookmarkContainerId); if (newGroup == null) return BadRequest(); diff --git a/Start/Server/Controllers/BookmarksController.cs b/Start/Server/Controllers/BookmarksController.cs index e4c84a1..4258ff7 100644 --- a/Start/Server/Controllers/BookmarksController.cs +++ b/Start/Server/Controllers/BookmarksController.cs @@ -37,9 +37,10 @@ namespace Start.Server.Controllers { [ProducesResponseType(StatusCodes.Status201Created, Type = typeof(BookmarkDto))] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task CreateBookmark(string title, string url, string? notes, - int bookmarkGroupId) { + int sortOrder, int bookmarkGroupId) { BookmarkDto? bookmark = (await this.bookmarkService - .CreateBookmark(this.GetAuthorizedUserId(), title, url, notes, bookmarkGroupId)) + .CreateBookmark(this.GetAuthorizedUserId(), title, url, notes, sortOrder, + bookmarkGroupId)) ?.MapToDto(); if (bookmark == null) diff --git a/Start/Server/Data/Migrations/20211220002043_AddSorting.Designer.cs b/Start/Server/Data/Migrations/20211220002043_AddSorting.Designer.cs new file mode 100644 index 0000000..6783563 --- /dev/null +++ b/Start/Server/Data/Migrations/20211220002043_AddSorting.Designer.cs @@ -0,0 +1,508 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Start.Server.Data; + +namespace Start.Server.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20211220002043_AddSorting")] + partial class AddSorting + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.11"); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => + { + b.Property("UserCode") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasMaxLength(50000) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DeviceCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Expiration") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("UserCode"); + + b.HasIndex("DeviceCode") + .IsUnique(); + + b.HasIndex("Expiration"); + + b.ToTable("DeviceCodes"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => + { + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedTime") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasMaxLength(50000) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Expiration") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.HasIndex("Expiration"); + + b.HasIndex("SubjectId", "ClientId", "Type"); + + b.HasIndex("SubjectId", "SessionId", "Type"); + + b.ToTable("PersistedGrants"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Start.Server.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Start.Server.Models.Bookmark", b => + { + b.Property("BookmarkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BookmarkGroupId") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("BookmarkId"); + + b.HasIndex("BookmarkGroupId"); + + b.ToTable("Bookmarks"); + }); + + modelBuilder.Entity("Start.Server.Models.BookmarkContainer", b => + { + b.Property("BookmarkContainerId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.HasKey("BookmarkContainerId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("BookmarkContainers"); + }); + + modelBuilder.Entity("Start.Server.Models.BookmarkGroup", b => + { + b.Property("BookmarkGroupId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BookmarkContainerId") + .HasColumnType("INTEGER"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(6) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.HasKey("BookmarkGroupId"); + + b.HasIndex("BookmarkContainerId"); + + b.ToTable("BookmarkGroups"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Start.Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Start.Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Start.Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Start.Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Start.Server.Models.Bookmark", b => + { + b.HasOne("Start.Server.Models.BookmarkGroup", "BookmarkGroup") + .WithMany("Bookmarks") + .HasForeignKey("BookmarkGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BookmarkGroup"); + }); + + modelBuilder.Entity("Start.Server.Models.BookmarkContainer", b => + { + b.HasOne("Start.Server.Models.ApplicationUser", "ApplicationUser") + .WithMany("BookmarkContainers") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApplicationUser"); + }); + + modelBuilder.Entity("Start.Server.Models.BookmarkGroup", b => + { + b.HasOne("Start.Server.Models.BookmarkContainer", "BookmarkContainer") + .WithMany("BookmarkGroups") + .HasForeignKey("BookmarkContainerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BookmarkContainer"); + }); + + modelBuilder.Entity("Start.Server.Models.ApplicationUser", b => + { + b.Navigation("BookmarkContainers"); + }); + + modelBuilder.Entity("Start.Server.Models.BookmarkContainer", b => + { + b.Navigation("BookmarkGroups"); + }); + + modelBuilder.Entity("Start.Server.Models.BookmarkGroup", b => + { + b.Navigation("Bookmarks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Start/Server/Data/Migrations/20211220002043_AddSorting.cs b/Start/Server/Data/Migrations/20211220002043_AddSorting.cs new file mode 100644 index 0000000..62637f2 --- /dev/null +++ b/Start/Server/Data/Migrations/20211220002043_AddSorting.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Start.Server.Data.Migrations { + public partial class AddSorting : Migration { + protected override void Up(MigrationBuilder migrationBuilder) { + migrationBuilder.AddColumn( + name: "SortOrder", + table: "Bookmarks", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "SortOrder", + table: "BookmarkGroups", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "SortOrder", + table: "BookmarkContainers", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropColumn( + name: "SortOrder", + table: "Bookmarks"); + + migrationBuilder.DropColumn( + name: "SortOrder", + table: "BookmarkGroups"); + + migrationBuilder.DropColumn( + name: "SortOrder", + table: "BookmarkContainers"); + } + } +} diff --git a/Start/Server/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/Start/Server/Data/Migrations/ApplicationDbContextModelSnapshot.cs index f01b9c2..560ad6a 100644 --- a/Start/Server/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Start/Server/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -328,6 +328,9 @@ namespace Start.Server.Data.Migrations .HasMaxLength(5000) .HasColumnType("TEXT"); + b.Property("SortOrder") + .HasColumnType("INTEGER"); + b.Property("Title") .IsRequired() .HasMaxLength(300) @@ -355,8 +358,12 @@ namespace Start.Server.Data.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("SortOrder") + .HasColumnType("INTEGER"); + b.Property("Title") .IsRequired() + .HasMaxLength(300) .HasColumnType("TEXT"); b.HasKey("BookmarkContainerId"); @@ -380,6 +387,9 @@ namespace Start.Server.Data.Migrations .HasMaxLength(6) .HasColumnType("TEXT"); + b.Property("SortOrder") + .HasColumnType("INTEGER"); + b.Property("Title") .IsRequired() .HasMaxLength(300) diff --git a/Start/Server/Data/Services/BookmarkContainerService.cs b/Start/Server/Data/Services/BookmarkContainerService.cs index 69f4aa2..de6a16f 100644 --- a/Start/Server/Data/Services/BookmarkContainerService.cs +++ b/Start/Server/Data/Services/BookmarkContainerService.cs @@ -46,10 +46,28 @@ namespace Start.Server.Data.Services { } public async Task CreateBookmarkContainer(string userId, - string title) { + string title, int sortOrder) { // No need to worry about ownership here - BookmarkContainer newContainer = new(userId, title); + // Increase the sorting ID for these items if it's needed to make room for this item + List? containers = this.db.BookmarkContainers + .Where(bc => bc.ApplicationUserId == userId) + .SortContainers() + .ToList(); + + if (containers == null) + return null; + + // Fix up sort order just in case + for (int i = 0; i < containers.Count; i++) { + containers[i].SortOrder = i; + + // Make room for the new container + if (i >= sortOrder) + containers[i].SortOrder++; + } + + BookmarkContainer newContainer = new(userId, title, sortOrder); await this.db.BookmarkContainers.AddAsync(newContainer); await this.db.SaveChangesAsync(); return newContainer; @@ -57,15 +75,39 @@ namespace Start.Server.Data.Services { public async Task UpdateBookmarkContainer(string userId, BookmarkContainer bookmarkContainer) { - BookmarkContainer? exitingBookmarkContainer = await this.db.BookmarkContainers - .SingleOrDefaultAsync(bc => bc.BookmarkContainerId - == bookmarkContainer.BookmarkContainerId); + BookmarkContainer? existingBookmarkContainer = await this.db.BookmarkContainers + .SingleOrDefaultAsync(bc => + bc.BookmarkContainerId == bookmarkContainer.BookmarkContainerId); - if (exitingBookmarkContainer == null + if (existingBookmarkContainer == null || !BookmarkOwnershipTools .IsBookmarkContainerOwner(this.db, userId, bookmarkContainer.BookmarkContainerId)) return null; + // If the sort order has changed, then the other containers need to be shuffled around + if (existingBookmarkContainer.SortOrder < bookmarkContainer.SortOrder) { + // The container has been moved to a higher sort order + var affectedContainers = db.BookmarkContainers + .Where(bc => bc.ApplicationUserId == userId) + .Where(bc => bc.SortOrder > existingBookmarkContainer.SortOrder) + .Where(bc => bc.SortOrder <= bookmarkContainer.SortOrder) + .ToList(); + + affectedContainers.ForEach(bc => bc.SortOrder -= 1); + // Let the save changes below save this + } + else if (existingBookmarkContainer.SortOrder > bookmarkContainer.SortOrder) { + // The container has been moved to a lower sort order + var affectedContainers = db.BookmarkContainers + .Where(bc => bc.ApplicationUserId == userId) + .Where(bc => bc.SortOrder < existingBookmarkContainer.SortOrder) + .Where(bc => bc.SortOrder >= bookmarkContainer.SortOrder) + .ToList(); + + affectedContainers.ForEach(bc => bc.SortOrder += 1); + // Let the save changes below save this + } + this.db.Entry(bookmarkContainer).State = EntityState.Modified; await this.db.SaveChangesAsync(); @@ -86,6 +128,22 @@ namespace Start.Server.Data.Services { this.db.BookmarkContainers.Remove(bookmarkContainer); await this.db.SaveChangesAsync(); + List? containers = this.db.BookmarkContainers + .Where(bc => bc.ApplicationUserId == userId) + .SortContainers() + .ToList(); + + if (containers == null) + // The container *was* deleted, so indicate as such + return true; + + // Fix up sort order just in case + for (int i = 0; i < containers.Count; i++) { + containers[i].SortOrder = i; + } + + await this.db.SaveChangesAsync(); + return true; } } diff --git a/Start/Server/Data/Services/BookmarkGroupService.cs b/Start/Server/Data/Services/BookmarkGroupService.cs index a700875..67a69a6 100644 --- a/Start/Server/Data/Services/BookmarkGroupService.cs +++ b/Start/Server/Data/Services/BookmarkGroupService.cs @@ -36,12 +36,12 @@ namespace Start.Server.Data.Services { } public async Task CreateBookmarkGroup(string userId, string title, - string color, int bookmarkContainerId) { + string color, int sortOrder, int bookmarkContainerId) { if (!BookmarkOwnershipTools .IsBookmarkContainerOwner(this.db, userId, bookmarkContainerId)) return null; - BookmarkGroup newBookmarkGroup = new(title, color, bookmarkContainerId); + BookmarkGroup newBookmarkGroup = new(title, color, sortOrder, bookmarkContainerId); await this.db.BookmarkGroups.AddAsync(newBookmarkGroup); await this.db.SaveChangesAsync(); diff --git a/Start/Server/Data/Services/BookmarkService.cs b/Start/Server/Data/Services/BookmarkService.cs index 0daf40a..e2e9b19 100644 --- a/Start/Server/Data/Services/BookmarkService.cs +++ b/Start/Server/Data/Services/BookmarkService.cs @@ -27,12 +27,12 @@ namespace Start.Server.Data.Services { .ToListAsync(); } - public async Task CreateBookmark(string userId, string title, string url, string? notes, - int bookmarkGroupId) { + public async Task CreateBookmark(string userId, string title, string url, + string? notes, int sortOrder, int bookmarkGroupId) { if (!BookmarkOwnershipTools.IsBookmarkGroupOwner(this.db, userId, bookmarkGroupId)) return null; - Bookmark newBookmark = new(title, url, notes, bookmarkGroupId); + Bookmark newBookmark = new(title, url, notes, sortOrder, bookmarkGroupId); await db.Bookmarks.AddAsync(newBookmark); await db.SaveChangesAsync(); diff --git a/Start/Server/Data/Services/Interfaces/IBookmarkContainerService.cs b/Start/Server/Data/Services/Interfaces/IBookmarkContainerService.cs index becc160..69d2ca9 100644 --- a/Start/Server/Data/Services/Interfaces/IBookmarkContainerService.cs +++ b/Start/Server/Data/Services/Interfaces/IBookmarkContainerService.cs @@ -10,7 +10,7 @@ namespace Start.Server.Data.Services.Interfaces { bool includeGroups = false, bool includeBookmarks = false); public Task CreateBookmarkContainer(string userId, - string title); + string title, int sortOrder); public Task UpdateBookmarkContainer(string userId, BookmarkContainer bookmarkContainer); public Task DeleteBookmarkContainer(string userId, int bookmarkContainerId); diff --git a/Start/Server/Data/Services/Interfaces/IBookmarkGroupService.cs b/Start/Server/Data/Services/Interfaces/IBookmarkGroupService.cs index 3ef1f20..1fbdf10 100644 --- a/Start/Server/Data/Services/Interfaces/IBookmarkGroupService.cs +++ b/Start/Server/Data/Services/Interfaces/IBookmarkGroupService.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using Start.Server.Models; -using Start.Shared; namespace Start.Server.Data.Services.Interfaces { public interface IBookmarkGroupService { @@ -12,7 +10,7 @@ namespace Start.Server.Data.Services.Interfaces { bool includeBookmarks = false); public Task CreateBookmarkGroup(string userId, string title, - string color, int bookmarkContainerId); + string color, int sortOrder, int bookmarkContainerId); public Task UpdateBookmarkGroup(string userId, BookmarkGroup bookmarkGroup); public Task DeleteBookmarkGroup(string userId, int bookmarkGroupId); diff --git a/Start/Server/Data/Services/Interfaces/IBookmarkService.cs b/Start/Server/Data/Services/Interfaces/IBookmarkService.cs index b7da850..ca2e437 100644 --- a/Start/Server/Data/Services/Interfaces/IBookmarkService.cs +++ b/Start/Server/Data/Services/Interfaces/IBookmarkService.cs @@ -8,7 +8,7 @@ namespace Start.Server.Data.Services.Interfaces { public Task> GetUserBookmarks(string userId); public Task CreateBookmark(string userId, string title, string url, - string? notes, int bookmarkGroupId); + string? notes, int sortOrder, int bookmarkGroupId); public Task UpdateBookmark(string userId, Bookmark bookmark); public Task DeleteBookmark(string userId, int bookmarkId); } diff --git a/Start/Server/Extensions/BookmarkMaps.cs b/Start/Server/Extensions/BookmarkMaps.cs index 63f771d..a0e73e3 100644 --- a/Start/Server/Extensions/BookmarkMaps.cs +++ b/Start/Server/Extensions/BookmarkMaps.cs @@ -6,18 +6,18 @@ namespace Start.Server.Extensions { public static class BookmarkMaps { public static BookmarkDto MapToDto(this Bookmark bookmark) { return new BookmarkDto(bookmark.BookmarkId, bookmark.Title, bookmark.Url, - bookmark.Notes, bookmark.BookmarkGroupId); + bookmark.Notes, bookmark.SortOrder, bookmark.BookmarkGroupId); } public static BookmarkGroupDto MapToDto(this BookmarkGroup bookmarkGroup) { return new BookmarkGroupDto(bookmarkGroup.BookmarkGroupId, bookmarkGroup.Title, - bookmarkGroup.Color, bookmarkGroup.BookmarkContainerId, + bookmarkGroup.Color, bookmarkGroup.SortOrder, bookmarkGroup.BookmarkContainerId, bookmarkGroup.Bookmarks?.Select(b => b.MapToDto()).ToList()); } public static BookmarkContainerDto MapToDto(this BookmarkContainer bookmarkContainer) { return new BookmarkContainerDto(bookmarkContainer.BookmarkContainerId, - bookmarkContainer.Title, + bookmarkContainer.Title, bookmarkContainer.SortOrder, bookmarkContainer.BookmarkGroups?.Select(bg => bg.MapToDto()).ToList()); } } diff --git a/Start/Server/Extensions/SortingExtensions.cs b/Start/Server/Extensions/SortingExtensions.cs new file mode 100644 index 0000000..93b84ae --- /dev/null +++ b/Start/Server/Extensions/SortingExtensions.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using Start.Server.Models; + +namespace Start.Server.Extensions { + public static class SortingExtensions { + public static IEnumerable SortContainers( + this IEnumerable bookmarkContainers) { + return bookmarkContainers + .OrderBy(bc => bc.SortOrder) + .ThenBy(bc => bc.BookmarkContainerId); + } + + public static IEnumerable SortGroups( + this IEnumerable bookmarkGroups) { + return bookmarkGroups + .OrderBy(bg => bg.SortOrder) + .ThenBy(bg => bg.BookmarkGroupId); + } + + public static IEnumerable SortBookmarks( + this IEnumerable bookmarks) { + return bookmarks + .OrderBy(b => b.SortOrder) + .ThenBy(b => b.BookmarkId); + } + } +} diff --git a/Start/Server/Models/Bookmark.cs b/Start/Server/Models/Bookmark.cs index 31b5fcc..70e5a68 100644 --- a/Start/Server/Models/Bookmark.cs +++ b/Start/Server/Models/Bookmark.cs @@ -1,5 +1,4 @@ -using System; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Start.Server.Models { /// A bookmark with display text and a URL to link to @@ -17,21 +16,24 @@ namespace Start.Server.Models { /// Arbitrary notes about the bookmark [MaxLength(5000)] public string? Notes { get; set; } + /// Used for sorting lists of bookmarks + public int SortOrder { get; set; } /// The unique ID for the group the bookmark is in public int BookmarkGroupId { get; set; } /// The group the bookmark is in public BookmarkGroup? BookmarkGroup { get; set; } - public Bookmark(string title, string url, string? notes, int bookmarkGroupId) { + public Bookmark(string title, string url, string? notes, int sortOrder, int bookmarkGroupId) { this.Title = title; this.Url = url; this.Notes = notes; + this.SortOrder = sortOrder; this.BookmarkGroupId = bookmarkGroupId; } - public Bookmark(int bookmarkId, string title, string url, string? notes, int bookmarkGroupId) - : this(title, url, notes, bookmarkGroupId) { + public Bookmark(int bookmarkId, string title, string url, string? notes, int sortOrder, + int bookmarkGroupId) : this(title, url, notes, sortOrder, bookmarkGroupId) { this.BookmarkId = bookmarkId; } } diff --git a/Start/Server/Models/BookmarkContainer.cs b/Start/Server/Models/BookmarkContainer.cs index e523fa8..d9512b6 100644 --- a/Start/Server/Models/BookmarkContainer.cs +++ b/Start/Server/Models/BookmarkContainer.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace Start.Server.Models { @@ -8,9 +7,11 @@ namespace Start.Server.Models { /// A unique ID for the container [Key] public int BookmarkContainerId { get; set; } - + /// A title to disply to the user [MaxLength(300)] public string Title { get; set; } + /// Used for sorting lists of bookmark containers + public int SortOrder { get; set; } /// The unique ID of the user that this container belongs to public string ApplicationUserId { get; set; } @@ -20,13 +21,14 @@ namespace Start.Server.Models { /// The s in this container public List? BookmarkGroups { get; set; } - public BookmarkContainer(string applicationUserId, string title) { + public BookmarkContainer(string applicationUserId, string title, int sortOrder) { this.ApplicationUserId = applicationUserId; this.Title = title; + this.SortOrder = sortOrder; } - public BookmarkContainer(int bookmarkContainerId, string applicationUserId, string title) - : this(applicationUserId, title) { + public BookmarkContainer(int bookmarkContainerId, string applicationUserId, string title, + int sortOrder) : this(applicationUserId, title, sortOrder) { this.BookmarkContainerId = bookmarkContainerId; } } diff --git a/Start/Server/Models/BookmarkGroup.cs b/Start/Server/Models/BookmarkGroup.cs index 49bb4e9..df63b1c 100644 --- a/Start/Server/Models/BookmarkGroup.cs +++ b/Start/Server/Models/BookmarkGroup.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace Start.Server.Models { @@ -15,6 +14,8 @@ namespace Start.Server.Models { /// A hex color for the group [MaxLength(6)] public string Color { get; set; } + /// Used for sorting lists of bookmark groups + public int SortOrder { get; set; } /// The unique ID of the container this group is in public int BookmarkContainerId { get; set; } @@ -24,14 +25,15 @@ namespace Start.Server.Models { /// The bookmarks in this group public List? Bookmarks { get; set; } - public BookmarkGroup(string title, string color, int bookmarkContainerId) { + public BookmarkGroup(string title, string color, int sortOrder, int bookmarkContainerId) { this.Title = title; this.Color = color; + this.SortOrder = sortOrder; this.BookmarkContainerId = bookmarkContainerId; } - public BookmarkGroup(int bookmarkGroupId, string title, string color, - int bookmarkContainerId) : this(title, color, bookmarkContainerId) { + public BookmarkGroup(int bookmarkGroupId, string title, string color, int sortOrder, + int bookmarkContainerId) : this(title, color, sortOrder, bookmarkContainerId) { this.BookmarkGroupId = bookmarkGroupId; } } diff --git a/Start/Shared/Api/IBookmarkContainersApi.cs b/Start/Shared/Api/IBookmarkContainersApi.cs index 054ba47..a20938a 100644 --- a/Start/Shared/Api/IBookmarkContainersApi.cs +++ b/Start/Shared/Api/IBookmarkContainersApi.cs @@ -12,8 +12,8 @@ namespace Start.Shared.Api { Task> GetBookmarkContainer(int bookmarkContainerId); [Post("/Create")] - Task> CreateBookmarkContainer( - [Body(BodySerializationMethod.Serialized)] string title); + Task> CreateBookmarkContainer(string title, + int sortOrder); [Delete("/Delete/{bookmarkContainerId}")] Task DeleteBookmarkContainer(int bookmarkContainerId); diff --git a/Start/Shared/Api/IBookmarkGroupsApi.cs b/Start/Shared/Api/IBookmarkGroupsApi.cs index 3368dba..9a5e8e4 100644 --- a/Start/Shared/Api/IBookmarkGroupsApi.cs +++ b/Start/Shared/Api/IBookmarkGroupsApi.cs @@ -9,7 +9,7 @@ namespace Start.Shared.Api { [Post("/Create")] Task> CreateBookmarkGroup(string title, string color, - int bookmarkContainerId); + int sortOrder, int bookmarkContainerId); [Delete("/Delete/{bookmarkGroupId}")] Task DeleteBookmarkGroup(int bookmarkGroupId); diff --git a/Start/Shared/Api/IBookmarksApi.cs b/Start/Shared/Api/IBookmarksApi.cs index 352f684..e7964d0 100644 --- a/Start/Shared/Api/IBookmarksApi.cs +++ b/Start/Shared/Api/IBookmarksApi.cs @@ -9,7 +9,7 @@ namespace Start.Shared.Api { [Post("/Create")] Task> CreateBookmark(string title, string url, string? notes, - int bookmarkGroupId); + int sortOrder, int bookmarkGroupId); [Delete("/Delete/{bookmarkId}")] Task DeleteBookmark(int bookmarkId); diff --git a/Start/Shared/BookmarkContainerDto.cs b/Start/Shared/BookmarkContainerDto.cs index 8e7d414..9480132 100644 --- a/Start/Shared/BookmarkContainerDto.cs +++ b/Start/Shared/BookmarkContainerDto.cs @@ -8,20 +8,23 @@ namespace Start.Shared { [Required] [StringLength(300)] public string Title { get; set; } + public int SortOrder { get; set; } public IList? BookmarkGroups { get; set; } - public BookmarkContainerDto(string title) { + public BookmarkContainerDto(string title, int sortOrder) { this.Title = title; + this.SortOrder = sortOrder; } - public BookmarkContainerDto(int bookmarkContainerId, string title) : this(title) { + public BookmarkContainerDto(int bookmarkContainerId, string title, int sortOrder) + : this(title, sortOrder) { this.BookmarkContainerId = bookmarkContainerId; } [JsonConstructor] - public BookmarkContainerDto(int bookmarkContainerId, string title, - IList? bookmarkGroups) : this(bookmarkContainerId, title) { + public BookmarkContainerDto(int bookmarkContainerId, string title, int sortOrder, + IList? bookmarkGroups) : this(bookmarkContainerId, title, sortOrder) { this.BookmarkGroups = bookmarkGroups; } } diff --git a/Start/Shared/BookmarkDto.cs b/Start/Shared/BookmarkDto.cs index 3468191..010feb0 100644 --- a/Start/Shared/BookmarkDto.cs +++ b/Start/Shared/BookmarkDto.cs @@ -10,20 +10,23 @@ namespace Start.Shared { public string Url { get; set; } [StringLength(5000)] public string? Notes { get; set; } + public int SortOrder { get; set; } public int BookmarkGroupId { get; set; } - public BookmarkDto(string title, string url, string? notes, int bookmarkGroupId) { + public BookmarkDto(string title, string url, string? notes, int sortOrder, + int bookmarkGroupId) { this.Title = title; this.Url = url; this.Notes = notes; + this.SortOrder = sortOrder; this.BookmarkGroupId = bookmarkGroupId; } [JsonConstructor] - public BookmarkDto(int bookmarkId, string title, string url, string? notes, + public BookmarkDto(int bookmarkId, string title, string url, string? notes, int sortOrder, int bookmarkGroupId) - : this(title, url, notes, bookmarkGroupId) { + : this(title, url, notes, sortOrder, bookmarkGroupId) { this.BookmarkId = bookmarkId; } } diff --git a/Start/Shared/BookmarkGroupDto.cs b/Start/Shared/BookmarkGroupDto.cs index 3a96699..da69307 100644 --- a/Start/Shared/BookmarkGroupDto.cs +++ b/Start/Shared/BookmarkGroupDto.cs @@ -11,26 +11,29 @@ namespace Start.Shared { [Required(AllowEmptyStrings = false, ErrorMessage = "Color is required")] [StringLength(7)] public string Color { get; set; } + public int SortOrder { get; set; } public int BookmarkContainerId { get; set; } public IList? Bookmarks { get; set; } - public BookmarkGroupDto(string title, string color, int bookmarkContainerId) { + public BookmarkGroupDto(string title, string color, int sortOrder, + int bookmarkContainerId) { this.Title = title; this.Color = color; + this.SortOrder = sortOrder; this.BookmarkContainerId = bookmarkContainerId; } - public BookmarkGroupDto(int bookmarkGroupId, string title, string color, + public BookmarkGroupDto(int bookmarkGroupId, string title, string color, int sortOrder, int bookmarkContainerId) - : this(title, color, bookmarkContainerId) { + : this(title, color, sortOrder, bookmarkContainerId) { this.BookmarkGroupId = bookmarkGroupId; } [JsonConstructor] - public BookmarkGroupDto(int bookmarkGroupId, string title, string color, + public BookmarkGroupDto(int bookmarkGroupId, string title, string color, int sortOrder, int bookmarkContainerId, IList? bookmarks) - : this(bookmarkGroupId, title, color, bookmarkContainerId) { + : this(bookmarkGroupId, title, color, sortOrder, bookmarkContainerId) { this.Bookmarks = bookmarks; } } diff --git a/Start/Shared/SortingExtensions.cs b/Start/Shared/SortingExtensions.cs new file mode 100644 index 0000000..52100fc --- /dev/null +++ b/Start/Shared/SortingExtensions.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Start.Shared { + public static class SortingExtensions { + public static IEnumerable SortContainers( + this IEnumerable bookmarkContainers) { + return bookmarkContainers + .OrderBy(bc => bc.SortOrder) + .ThenBy(bc => bc.BookmarkContainerId); + } + + public static IEnumerable SortGroups( + this IEnumerable bookmarkGroups) { + return bookmarkGroups + .OrderBy(bg => bg.SortOrder) + .ThenBy(bg => bg.BookmarkGroupId); + } + + public static IEnumerable SortBookmarks( + this IEnumerable bookmarks) { + return bookmarks + .OrderBy(b => b.SortOrder) + .ThenBy(b => b.BookmarkId); + } + } +}