Add sorting columns

This commit is contained in:
Neil Brommer 2022-04-19 13:04:38 -07:00
parent adf24cbd5c
commit 90adbcfb7c
34 changed files with 833 additions and 80 deletions

View file

@ -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

View file

@ -73,13 +73,13 @@
</Dialog>
@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<ShowCreateBookmarkFormAction>(this,
a => this.Model.BookmarkGroupId = a.GroupId);

View file

@ -65,7 +65,7 @@
[Parameter]
public EventCallback<BookmarkContainerDto> OnCreated { get; set; }
private BookmarkContainerDto Model { get; set; } = new BookmarkContainerDto("");
private BookmarkContainerDto Model { get; set; } = new BookmarkContainerDto("", 0);
protected void OnSubmit()
{

View file

@ -63,13 +63,13 @@
</Dialog>
@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<ShowCreateGroupFormAction>(this,

View file

@ -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()
}
};

View file

@ -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<Start.Shared.BookmarkGroupDto?>? 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<Start.Shared.BookmarkDto?>? 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(

View file

@ -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<IEnumerable<BookmarkContainerDto>>? 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<BookmarkContainerDto?> apiResponse = await this.BookmarkContainersApi
.CreateBookmarkContainer(action.NewContainer.Title);
.CreateBookmarkContainer(action.NewContainer.Title, sortOrder);
BookmarkContainerDto? container = apiResponse.Content;

View file

@ -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<BookmarkContainerDto?>? 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<BookmarkGroupDto?> 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);

View file

@ -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;
}

View file

@ -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<BookmarkGroupDto> { 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<BookmarkDto> { 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<BookmarkGroupDto>? 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)
}
};
}

View file

@ -54,9 +54,9 @@ namespace Start.Server.Controllers {
[Route("Create")]
[ProducesResponseType(StatusCodes.Status201Created, Type = typeof(BookmarkContainerDto))]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateBookmarkContainer([FromBody] string title) {
public async Task<IActionResult> CreateBookmarkContainer(string title, int sortOrder) {
BookmarkContainerDto? container = (await this.bookmarkContainerService
.CreateBookmarkContainer(this.GetAuthorizedUserId(), title))
.CreateBookmarkContainer(this.GetAuthorizedUserId(), title, sortOrder))
?.MapToDto();
if (container == null)

View file

@ -37,9 +37,9 @@ namespace Start.Server.Controllers {
[ProducesResponseType(StatusCodes.Status201Created, Type = typeof(BookmarkGroupDto))]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> 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();

View file

@ -37,9 +37,10 @@ namespace Start.Server.Controllers {
[ProducesResponseType(StatusCodes.Status201Created, Type = typeof(BookmarkDto))]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> 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)

View file

@ -0,0 +1,508 @@
// <auto-generated />
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<string>("UserCode")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTime>("CreationTime")
.HasColumnType("TEXT");
b.Property<string>("Data")
.IsRequired()
.HasMaxLength(50000)
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DeviceCode")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTime?>("Expiration")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("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<string>("Key")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTime?>("ConsumedTime")
.HasColumnType("TEXT");
b.Property<DateTime>("CreationTime")
.HasColumnType("TEXT");
b.Property<string>("Data")
.IsRequired()
.HasMaxLength(50000)
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTime?>("Expiration")
.HasColumnType("TEXT");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SubjectId")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("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<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Start.Server.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("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<int>("BookmarkId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("BookmarkGroupId")
.HasColumnType("INTEGER");
b.Property<string>("Notes")
.HasMaxLength(5000)
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("TEXT");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.HasKey("BookmarkId");
b.HasIndex("BookmarkGroupId");
b.ToTable("Bookmarks");
});
modelBuilder.Entity("Start.Server.Models.BookmarkContainer", b =>
{
b.Property<int>("BookmarkContainerId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("TEXT");
b.HasKey("BookmarkContainerId");
b.HasIndex("ApplicationUserId");
b.ToTable("BookmarkContainers");
});
modelBuilder.Entity("Start.Server.Models.BookmarkGroup", b =>
{
b.Property<int>("BookmarkGroupId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("BookmarkContainerId")
.HasColumnType("INTEGER");
b.Property<string>("Color")
.IsRequired()
.HasMaxLength(6)
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("TEXT");
b.HasKey("BookmarkGroupId");
b.HasIndex("BookmarkContainerId");
b.ToTable("BookmarkGroups");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Start.Server.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Start.Server.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", 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
}
}
}

View file

@ -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<int>(
name: "SortOrder",
table: "Bookmarks",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "SortOrder",
table: "BookmarkGroups",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
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");
}
}
}

View file

@ -328,6 +328,9 @@ namespace Start.Server.Data.Migrations
.HasMaxLength(5000)
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
@ -355,8 +358,12 @@ namespace Start.Server.Data.Migrations
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("TEXT");
b.HasKey("BookmarkContainerId");
@ -380,6 +387,9 @@ namespace Start.Server.Data.Migrations
.HasMaxLength(6)
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)

View file

@ -46,10 +46,28 @@ namespace Start.Server.Data.Services {
}
public async Task<BookmarkContainer?> 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<BookmarkContainer>? 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<BookmarkContainer?> 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<BookmarkContainer>? 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;
}
}

View file

@ -36,12 +36,12 @@ namespace Start.Server.Data.Services {
}
public async Task<BookmarkGroup?> 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();

View file

@ -27,12 +27,12 @@ namespace Start.Server.Data.Services {
.ToListAsync();
}
public async Task<Bookmark?> CreateBookmark(string userId, string title, string url, string? notes,
int bookmarkGroupId) {
public async Task<Bookmark?> 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();

View file

@ -10,7 +10,7 @@ namespace Start.Server.Data.Services.Interfaces {
bool includeGroups = false, bool includeBookmarks = false);
public Task<BookmarkContainer?> CreateBookmarkContainer(string userId,
string title);
string title, int sortOrder);
public Task<BookmarkContainer?> UpdateBookmarkContainer(string userId,
BookmarkContainer bookmarkContainer);
public Task<bool> DeleteBookmarkContainer(string userId, int bookmarkContainerId);

View file

@ -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<BookmarkGroup?> CreateBookmarkGroup(string userId, string title,
string color, int bookmarkContainerId);
string color, int sortOrder, int bookmarkContainerId);
public Task<BookmarkGroup?> UpdateBookmarkGroup(string userId,
BookmarkGroup bookmarkGroup);
public Task<bool> DeleteBookmarkGroup(string userId, int bookmarkGroupId);

View file

@ -8,7 +8,7 @@ namespace Start.Server.Data.Services.Interfaces {
public Task<IList<Bookmark>> GetUserBookmarks(string userId);
public Task<Bookmark?> CreateBookmark(string userId, string title, string url,
string? notes, int bookmarkGroupId);
string? notes, int sortOrder, int bookmarkGroupId);
public Task<Bookmark?> UpdateBookmark(string userId, Bookmark bookmark);
public Task<bool> DeleteBookmark(string userId, int bookmarkId);
}

View file

@ -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());
}
}

View file

@ -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<BookmarkContainer> SortContainers(
this IEnumerable<BookmarkContainer> bookmarkContainers) {
return bookmarkContainers
.OrderBy(bc => bc.SortOrder)
.ThenBy(bc => bc.BookmarkContainerId);
}
public static IEnumerable<BookmarkGroup> SortGroups(
this IEnumerable<BookmarkGroup> bookmarkGroups) {
return bookmarkGroups
.OrderBy(bg => bg.SortOrder)
.ThenBy(bg => bg.BookmarkGroupId);
}
public static IEnumerable<Bookmark> SortBookmarks(
this IEnumerable<Bookmark> bookmarks) {
return bookmarks
.OrderBy(b => b.SortOrder)
.ThenBy(b => b.BookmarkId);
}
}
}

View file

@ -1,5 +1,4 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
namespace Start.Server.Models {
/// <summary>A bookmark with display text and a URL to link to</summary>
@ -17,21 +16,24 @@ namespace Start.Server.Models {
/// <summary>Arbitrary notes about the bookmark</summary>
[MaxLength(5000)]
public string? Notes { get; set; }
/// <summary>Used for sorting lists of bookmarks</summary>
public int SortOrder { get; set; }
/// <summary>The unique ID for the group the bookmark is in</summary>
public int BookmarkGroupId { get; set; }
/// <summary>The group the bookmark is in</summary>
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;
}
}

View file

@ -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 {
/// <summary>A unique ID for the container</summary>
[Key]
public int BookmarkContainerId { get; set; }
/// <summary>A title to disply to the user</summary>
[MaxLength(300)]
public string Title { get; set; }
/// <summary>Used for sorting lists of bookmark containers</summary>
public int SortOrder { get; set; }
/// <summary>The unique ID of the user that this container belongs to</summary>
public string ApplicationUserId { get; set; }
@ -20,13 +21,14 @@ namespace Start.Server.Models {
/// <summary>The <see cref="BookmarkGroup"/>s in this container</summary>
public List<BookmarkGroup>? 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;
}
}

View file

@ -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 {
/// <summary>A hex color for the group</summary>
[MaxLength(6)]
public string Color { get; set; }
/// <summary>Used for sorting lists of bookmark groups</summary>
public int SortOrder { get; set; }
/// <summary>The unique ID of the container this group is in</summary>
public int BookmarkContainerId { get; set; }
@ -24,14 +25,15 @@ namespace Start.Server.Models {
/// <summary>The bookmarks in this group</summary>
public List<Bookmark>? 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;
}
}

View file

@ -12,8 +12,8 @@ namespace Start.Shared.Api {
Task<ApiResponse<BookmarkContainerDto?>> GetBookmarkContainer(int bookmarkContainerId);
[Post("/Create")]
Task<ApiResponse<BookmarkContainerDto?>> CreateBookmarkContainer(
[Body(BodySerializationMethod.Serialized)] string title);
Task<ApiResponse<BookmarkContainerDto?>> CreateBookmarkContainer(string title,
int sortOrder);
[Delete("/Delete/{bookmarkContainerId}")]
Task<HttpResponseMessage> DeleteBookmarkContainer(int bookmarkContainerId);

View file

@ -9,7 +9,7 @@ namespace Start.Shared.Api {
[Post("/Create")]
Task<ApiResponse<BookmarkGroupDto?>> CreateBookmarkGroup(string title, string color,
int bookmarkContainerId);
int sortOrder, int bookmarkContainerId);
[Delete("/Delete/{bookmarkGroupId}")]
Task<HttpResponseMessage> DeleteBookmarkGroup(int bookmarkGroupId);

View file

@ -9,7 +9,7 @@ namespace Start.Shared.Api {
[Post("/Create")]
Task<ApiResponse<BookmarkDto?>> CreateBookmark(string title, string url, string? notes,
int bookmarkGroupId);
int sortOrder, int bookmarkGroupId);
[Delete("/Delete/{bookmarkId}")]
Task<HttpResponseMessage> DeleteBookmark(int bookmarkId);

View file

@ -8,20 +8,23 @@ namespace Start.Shared {
[Required]
[StringLength(300)]
public string Title { get; set; }
public int SortOrder { get; set; }
public IList<BookmarkGroupDto>? 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<BookmarkGroupDto>? bookmarkGroups) : this(bookmarkContainerId, title) {
public BookmarkContainerDto(int bookmarkContainerId, string title, int sortOrder,
IList<BookmarkGroupDto>? bookmarkGroups) : this(bookmarkContainerId, title, sortOrder) {
this.BookmarkGroups = bookmarkGroups;
}
}

View file

@ -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;
}
}

View file

@ -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<BookmarkDto>? 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<BookmarkDto>? bookmarks)
: this(bookmarkGroupId, title, color, bookmarkContainerId) {
: this(bookmarkGroupId, title, color, sortOrder, bookmarkContainerId) {
this.Bookmarks = bookmarks;
}
}

View file

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Linq;
namespace Start.Shared {
public static class SortingExtensions {
public static IEnumerable<BookmarkContainerDto> SortContainers(
this IEnumerable<BookmarkContainerDto> bookmarkContainers) {
return bookmarkContainers
.OrderBy(bc => bc.SortOrder)
.ThenBy(bc => bc.BookmarkContainerId);
}
public static IEnumerable<BookmarkGroupDto> SortGroups(
this IEnumerable<BookmarkGroupDto> bookmarkGroups) {
return bookmarkGroups
.OrderBy(bg => bg.SortOrder)
.ThenBy(bg => bg.BookmarkGroupId);
}
public static IEnumerable<BookmarkDto> SortBookmarks(
this IEnumerable<BookmarkDto> bookmarks) {
return bookmarks
.OrderBy(b => b.SortOrder)
.ThenBy(b => b.BookmarkId);
}
}
}