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

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