From 6fd5b7cfa75a555b92fa2ec2d114d7ea5b9cdacc Mon Sep 17 00:00:00 2001 From: PaddiM8 Date: Sun, 11 Aug 2024 00:17:34 +0200 Subject: [PATCH] Add dark theme --- api/Controllers/UserController.cs | 3 +- api/Data/Dto/AccountDto.cs | 2 + api/Data/Dto/UserDto.cs | 2 + api/Data/InterfaceTheme.cs | 7 + api/Data/User.cs | 2 + api/Hubs/IUserHubContext.cs | 3 + api/Hubs/UserHub.cs | 36 +- .../20240810132803_UserTheme.Designer.cs | 584 ++++++++++++++++++ api/Migrations/20240810132803_UserTheme.cs | 29 + api/Migrations/DataContextModelSnapshot.cs | 3 + api/Models/User/EditUserModel.cs | 3 + api/Program.cs | 1 + api/Services/UserService.cs | 96 ++- web/src/gen/planeraClient.ts | 17 + .../lib/components/ContextMenuEntry.svelte | 2 +- web/src/lib/components/IconButton.svelte | 1 + web/src/lib/components/editor/editor.css | 207 +++++-- .../components/editor/ticketEditorTheme.ts | 176 +++--- web/src/lib/components/form/BlockInput.svelte | 5 +- web/src/lib/components/form/Input.svelte | 1 + web/src/lib/components/form/Select.svelte | 2 + .../lib/components/form/SuggestionList.svelte | 26 +- .../components/sidebar/SidebarEntry.svelte | 2 +- .../lib/components/ticket/TicketEntry.svelte | 6 +- web/src/lib/util.ts | 4 +- web/src/routes/(auth)/logout/+page.server.ts | 2 +- .../routes/(main)/invitations/+page.svelte | 2 +- .../projects/[user]/[slug]/+page.svelte | 11 +- .../[user]/[slug]/settings/+page.server.ts | 16 +- .../+layout.svelte | 23 +- .../account}/+page.server.ts | 2 +- .../account}/+page.svelte | 6 +- .../(other)/settings/general/+page.server.ts | 18 + .../(other)/settings/general/+page.svelte | 43 ++ web/src/routes/(other)/settings/store.ts | 4 + web/src/routes/+layout.server.ts | 5 +- web/src/routes/+layout.svelte | 76 ++- web/src/routes/store.ts | 4 + web/static/themes/dark.css | 35 ++ web/static/themes/light.css | 35 ++ 40 files changed, 1195 insertions(+), 307 deletions(-) create mode 100644 api/Data/InterfaceTheme.cs create mode 100644 api/Migrations/20240810132803_UserTheme.Designer.cs create mode 100644 api/Migrations/20240810132803_UserTheme.cs rename web/src/routes/(other)/{user-settings => settings}/+layout.svelte (57%) rename web/src/routes/(other)/{user-settings => settings/account}/+page.server.ts (96%) rename web/src/routes/(other)/{user-settings => settings/account}/+page.svelte (94%) create mode 100644 web/src/routes/(other)/settings/general/+page.server.ts create mode 100644 web/src/routes/(other)/settings/general/+page.svelte create mode 100644 web/src/routes/(other)/settings/store.ts create mode 100644 web/src/routes/store.ts create mode 100644 web/static/themes/dark.css create mode 100644 web/static/themes/light.css diff --git a/api/Controllers/UserController.cs b/api/Controllers/UserController.cs index 2e997b8..526726c 100644 --- a/api/Controllers/UserController.cs +++ b/api/Controllers/UserController.cs @@ -44,7 +44,8 @@ public async Task Edit([FromBody] EditUserModel model) User.FindFirst("Id")!.Value, model.Username, model.Email, - model.Avatar + model.Avatar, + model.Theme ); return result.ToActionResult(); diff --git a/api/Data/Dto/AccountDto.cs b/api/Data/Dto/AccountDto.cs index 9b80b0c..3ed1ae8 100644 --- a/api/Data/Dto/AccountDto.cs +++ b/api/Data/Dto/AccountDto.cs @@ -9,4 +9,6 @@ public class AccountDto public required string Email { get; set; } public string? AvatarPath { get; set; } + + public InterfaceTheme Theme { get; set; } } \ No newline at end of file diff --git a/api/Data/Dto/UserDto.cs b/api/Data/Dto/UserDto.cs index 1fa6a15..98bb078 100644 --- a/api/Data/Dto/UserDto.cs +++ b/api/Data/Dto/UserDto.cs @@ -7,4 +7,6 @@ public class UserDto public required string Username { get; init; } public string? AvatarPath { get; init; } + + public InterfaceTheme Theme { get; set; } } \ No newline at end of file diff --git a/api/Data/InterfaceTheme.cs b/api/Data/InterfaceTheme.cs new file mode 100644 index 0000000..9a76a0a --- /dev/null +++ b/api/Data/InterfaceTheme.cs @@ -0,0 +1,7 @@ +namespace Planera.Data; + +public enum InterfaceTheme +{ + Light, + Dark, +} \ No newline at end of file diff --git a/api/Data/User.cs b/api/Data/User.cs index c510e2d..7a2d25b 100644 --- a/api/Data/User.cs +++ b/api/Data/User.cs @@ -7,6 +7,8 @@ public class User : IdentityUser { public string? AvatarPath { get; set; } + public InterfaceTheme Theme { get; set; } + public ICollection Projects { get; init; } = new List(); public ICollection Tickets { get; set; } = new List(); diff --git a/api/Hubs/IUserHubContext.cs b/api/Hubs/IUserHubContext.cs index 9a4252e..11b55ea 100644 --- a/api/Hubs/IUserHubContext.cs +++ b/api/Hubs/IUserHubContext.cs @@ -1,3 +1,4 @@ +using Planera.Data; using Planera.Data.Dto; namespace Planera.Hubs; @@ -7,4 +8,6 @@ public interface IUserHubContext public Task OnAddProject(ProjectDto project); public Task OnAddInvitation(ProjectDto project); + + public Task SetTheme(InterfaceTheme theme); } \ No newline at end of file diff --git a/api/Hubs/UserHub.cs b/api/Hubs/UserHub.cs index fcf6259..3e6fc35 100644 --- a/api/Hubs/UserHub.cs +++ b/api/Hubs/UserHub.cs @@ -1,40 +1,46 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; +using Planera.Data; using Planera.Extensions; using Planera.Services; namespace Planera.Hubs; [Authorize] -public class UserHub : Hub +public class UserHub(UserService userService, IHubContext projectHub) + : Hub { - private readonly UserService _userService; - private readonly IHubContext _projectHub; - - public UserHub(UserService userService, IHubContext projectHub) - { - _userService = userService; - _projectHub = projectHub; - } - public async Task AcceptInvitation(string projectId) { - string userId = Context.User!.FindFirst("Id")!.Value; - var result = await _userService.AcceptInvitation(userId, projectId); + var userId = Context.User!.FindFirst("Id")!.Value; + var result = await userService.AcceptInvitation(userId, projectId); var invitation = result.Unwrap(); await Clients .User(Context.User!.Identity!.Name!) .OnAddProject(invitation.Project); - await _projectHub.Clients + await projectHub.Clients .Group(projectId) .OnAddParticipant(invitation.User); } public async Task DeclineInvitation(string projectId) { - string userId = Context.User!.FindFirst("Id")!.Value; - var result = await _userService.DeclineInvitation(userId, projectId); + var userId = Context.User!.FindFirst("Id")!.Value; + var result = await userService.DeclineInvitation(userId, projectId); + result.Unwrap(); + } + + public async Task SetTheme(InterfaceTheme theme) + { + var userId = Context.User!.FindFirst("Id")!.Value; + var result = await userService.EditAsync( + userId, + null, + null, + null, + theme + ); result.Unwrap(); } } \ No newline at end of file diff --git a/api/Migrations/20240810132803_UserTheme.Designer.cs b/api/Migrations/20240810132803_UserTheme.Designer.cs new file mode 100644 index 0000000..56139fa --- /dev/null +++ b/api/Migrations/20240810132803_UserTheme.Designer.cs @@ -0,0 +1,584 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Planera.Data; + +#nullable disable + +namespace Planera.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20240810132803_UserTheme")] + partial class UserTheme + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pg_trgm"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + 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", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Planera.Data.Invitation", b => + { + b.Property("ProjectId") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("ProjectId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("Invitations"); + }); + + modelBuilder.Entity("Planera.Data.Note", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TicketId") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TicketId", "ProjectId"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("Planera.Data.Project", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EnableTicketAssignees") + .HasColumnType("boolean"); + + b.Property("EnableTicketDescriptions") + .HasColumnType("boolean"); + + b.Property("IconPath") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId", "Slug") + .IsUnique(); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("Planera.Data.ProjectParticipant", b => + { + b.Property("ProjectId") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Filter") + .HasColumnType("integer"); + + b.Property("Sorting") + .HasColumnType("integer"); + + b.HasKey("ProjectId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ProjectParticipants"); + }); + + modelBuilder.Entity("Planera.Data.Ticket", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ProjectId") + .HasColumnType("text"); + + b.Property("AuthorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id", "ProjectId"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("Title", "Description") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Title", "Description"), "gin"); + NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("Title", "Description"), new[] { "gin_trgm_ops" }); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Planera.Data.TicketAssignee", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("TicketId") + .HasColumnType("integer"); + + b.Property("TicketProjectId") + .HasColumnType("text"); + + b.HasKey("UserId", "TicketId", "TicketProjectId"); + + b.HasIndex("TicketId", "TicketProjectId"); + + b.ToTable("TicketAssignees"); + }); + + modelBuilder.Entity("Planera.Data.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("AvatarPath") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("Theme") + .HasColumnType("integer"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + 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("Planera.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Planera.Data.User", 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("Planera.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Planera.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Planera.Data.Invitation", b => + { + b.HasOne("Planera.Data.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Planera.Data.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Planera.Data.Note", b => + { + b.HasOne("Planera.Data.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Planera.Data.Ticket", "Ticket") + .WithMany("Notes") + .HasForeignKey("TicketId", "ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Planera.Data.Project", b => + { + b.HasOne("Planera.Data.User", "Author") + .WithMany("Projects") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Planera.Data.ProjectParticipant", b => + { + b.HasOne("Planera.Data.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Planera.Data.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Planera.Data.Ticket", b => + { + b.HasOne("Planera.Data.User", "Author") + .WithMany("Tickets") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Planera.Data.Project", "Project") + .WithMany("Tickets") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Planera.Data.TicketAssignee", b => + { + b.HasOne("Planera.Data.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Planera.Data.Ticket", "Ticket") + .WithMany() + .HasForeignKey("TicketId", "TicketProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Planera.Data.Project", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("Planera.Data.Ticket", b => + { + b.Navigation("Notes"); + }); + + modelBuilder.Entity("Planera.Data.User", b => + { + b.Navigation("Projects"); + + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api/Migrations/20240810132803_UserTheme.cs b/api/Migrations/20240810132803_UserTheme.cs new file mode 100644 index 0000000..06e40c4 --- /dev/null +++ b/api/Migrations/20240810132803_UserTheme.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Planera.Migrations +{ + /// + public partial class UserTheme : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Theme", + table: "AspNetUsers", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Theme", + table: "AspNetUsers"); + } + } +} diff --git a/api/Migrations/DataContextModelSnapshot.cs b/api/Migrations/DataContextModelSnapshot.cs index 392eb15..01e059c 100644 --- a/api/Migrations/DataContextModelSnapshot.cs +++ b/api/Migrations/DataContextModelSnapshot.cs @@ -380,6 +380,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("SecurityStamp") .HasColumnType("text"); + b.Property("Theme") + .HasColumnType("integer"); + b.Property("TwoFactorEnabled") .HasColumnType("boolean"); diff --git a/api/Models/User/EditUserModel.cs b/api/Models/User/EditUserModel.cs index 85336b8..945eed7 100644 --- a/api/Models/User/EditUserModel.cs +++ b/api/Models/User/EditUserModel.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Planera.Data; namespace Planera.Models.User; @@ -17,4 +18,6 @@ public class EditUserModel public required string Email { get; init; } public string? Avatar { get; init; } + + public InterfaceTheme? Theme { get; init; } } \ No newline at end of file diff --git a/api/Program.cs b/api/Program.cs index 2a168e7..67180ea 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; using NSwag.Generation; using Planera; diff --git a/api/Services/UserService.cs b/api/Services/UserService.cs index 2b6a4f5..9bee434 100644 --- a/api/Services/UserService.cs +++ b/api/Services/UserService.cs @@ -9,56 +9,47 @@ namespace Planera.Services; -public class UserService +public class UserService( + DataContext dataContext, + IMapper mapper, + UserManager userManager, + IFileStorage fileStorage) { - private readonly DataContext _dataContext; - private readonly IMapper _mapper; - private readonly UserManager _userManager; - private readonly IFileStorage _fileStorage; - - public UserService( - DataContext dataContext, - IMapper mapper, - UserManager userManager, - IFileStorage fileStorage) - { - _dataContext = dataContext; - _mapper = mapper; - _userManager = userManager; - _fileStorage = fileStorage; - } - public async Task> GetAsync(string userId) { - var user = await _dataContext.Users.FindAsync(userId); + var user = await dataContext.Users.FindAsync(userId); if (user == null) return Error.NotFound("UserId.NotFound", "A user with the given ID was not found."); - return _mapper.Map(user); + return mapper.Map(user); } public async Task> GetAccountAsync(string userId) { - var user = await _dataContext.Users.FindAsync(userId); + var user = await dataContext.Users.FindAsync(userId); if (user == null) return Error.NotFound("UserId.NotFound", "A user with the given ID was not found."); - return _mapper.Map(user); + return mapper.Map(user); } public async Task> EditAsync( string userId, - string username, - string email, - string? avatar) + string? username, + string? email, + string? avatar, + InterfaceTheme? theme) { - var user = await _dataContext.Users.FindAsync(userId); + var user = await dataContext.Users.FindAsync(userId); if (user == null) return Error.NotFound("UserId.NotFound", "A user with the given ID was not found."); - var existingByName = await _userManager.FindByNameAsync(username); - if (existingByName != null && existingByName.Id != userId) - return Error.Conflict("Username.Taken", "Another user with the given username already exists."); + if (username != null) + { + var existingByName = await userManager.FindByNameAsync(username); + if (existingByName != null && existingByName.Id != userId) + return Error.Conflict("Username.Taken", "Another user with the given username already exists."); + } var previousAvatarPath = user.AvatarPath; if (avatar?.StartsWith("data:") is true) @@ -67,7 +58,7 @@ public async Task> EditAsync( var bytes = Convert.FromBase64String(avatar.Split(",")[1]); var avatar256 = ImagePreparer.Resize(bytes, 256, 256); var avatar32 = ImagePreparer.Resize(bytes, 32, 32); - user.AvatarPath = await _fileStorage.WriteManyAsync( + user.AvatarPath = await fileStorage.WriteManyAsync( "avatars", (avatar256, "256"), (avatar32, "32") @@ -78,17 +69,24 @@ public async Task> EditAsync( user.AvatarPath = null; } - user.UserName = username; - user.Email = email; - var result = await _userManager.UpdateAsync(user); + if (username != null) + user.UserName = username; + + if (email != null) + user.Email = email; + + if (theme != null) + user.Theme = theme.Value; + + var result = await userManager.UpdateAsync(user); if (!result.Succeeded) { // If it didn't update, remove the newly created files, since // they won't be used. if (!string.IsNullOrEmpty(user.AvatarPath)) { - _fileStorage.Delete(user.AvatarPath, "32"); - _fileStorage.Delete(user.AvatarPath, "256"); + fileStorage.Delete(user.AvatarPath, "32"); + fileStorage.Delete(user.AvatarPath, "256"); } return Error.Unexpected("Unknown", "Failed to update user."); @@ -96,8 +94,8 @@ public async Task> EditAsync( if (previousAvatarPath != null && previousAvatarPath != user.AvatarPath) { - _fileStorage.Delete(previousAvatarPath, "32"); - _fileStorage.Delete(previousAvatarPath, "256"); + fileStorage.Delete(previousAvatarPath, "32"); + fileStorage.Delete(previousAvatarPath, "256"); } return new ErrorOr(); @@ -105,8 +103,8 @@ public async Task> EditAsync( public async Task> ChangePasswordAsync(string userId, string currentPassword, string newPassword) { - var user = await _userManager.FindByIdAsync(userId); - var identityResult = await _userManager.ChangePasswordAsync(user!, currentPassword, newPassword); + var user = await userManager.FindByIdAsync(userId); + var identityResult = await userManager.ChangePasswordAsync(user!, currentPassword, newPassword); return !identityResult.Succeeded ? Error.Validation("CurrentPassword.Invalid", "Incorrect password.") @@ -115,16 +113,16 @@ public async Task> ChangePasswordAsync(string userId, string cu public async Task>> GetInvitations(string userId) { - return await _dataContext.Invitations + return await dataContext.Invitations .Where(x => x.UserId == userId) .Select(x => x.Project) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .ToListAsync(); } public async Task> AcceptInvitation(string userId, string projectId) { - var invitation = await _dataContext.Invitations + var invitation = await dataContext.Invitations .Where(x => x.UserId == userId) .Include(x => x.User) .Where(x => x.ProjectId == projectId) @@ -134,28 +132,28 @@ public async Task> AcceptInvitation(string userId, string if (invitation == null) return Error.NotFound("Invitation.NotFound", "Invitation was not found."); - await _dataContext.ProjectParticipants.AddAsync(new ProjectParticipant + await dataContext.ProjectParticipants.AddAsync(new ProjectParticipant { UserId = userId, ProjectId = projectId, }); - _dataContext.Invitations.Remove(invitation); - await _dataContext.SaveChangesAsync(); + dataContext.Invitations.Remove(invitation); + await dataContext.SaveChangesAsync(); - return _mapper.Map(invitation); + return mapper.Map(invitation); } public async Task> DeclineInvitation(string userId, string projectId) { - var invitation = await _dataContext.Invitations + var invitation = await dataContext.Invitations .Where(x => x.UserId == userId) .Where(x => x.ProjectId == projectId) .SingleOrDefaultAsync(); if (invitation == null) return Error.NotFound("Invitation.NotFound", "Invitation was not found."); - _dataContext.Invitations.Remove(invitation); - await _dataContext.SaveChangesAsync(); + dataContext.Invitations.Remove(invitation); + await dataContext.SaveChangesAsync(); return new ErrorOr(); } diff --git a/web/src/gen/planeraClient.ts b/web/src/gen/planeraClient.ts index 532cc0e..707d1a4 100644 --- a/web/src/gen/planeraClient.ts +++ b/web/src/gen/planeraClient.ts @@ -1657,6 +1657,7 @@ export class UserDto implements IUserDto { id?: string; username?: string; avatarPath?: string; + theme?: InterfaceTheme; constructor(data?: IUserDto) { if (data) { @@ -1672,6 +1673,7 @@ export class UserDto implements IUserDto { this.id = _data["id"]; this.username = _data["username"]; this.avatarPath = _data["avatarPath"]; + this.theme = _data["theme"]; } } @@ -1687,6 +1689,7 @@ export class UserDto implements IUserDto { data["id"] = this.id; data["username"] = this.username; data["avatarPath"] = this.avatarPath; + data["theme"] = this.theme; return data; } } @@ -1695,6 +1698,12 @@ export interface IUserDto { id?: string; username?: string; avatarPath?: string; + theme?: InterfaceTheme; +} + +export enum InterfaceTheme { + Light = 0, + Dark = 1, } export class LoginModel implements ILoginModel { @@ -2461,6 +2470,7 @@ export class AccountDto implements IAccountDto { username?: string; email?: string; avatarPath?: string; + theme?: InterfaceTheme; constructor(data?: IAccountDto) { if (data) { @@ -2477,6 +2487,7 @@ export class AccountDto implements IAccountDto { this.username = _data["username"]; this.email = _data["email"]; this.avatarPath = _data["avatarPath"]; + this.theme = _data["theme"]; } } @@ -2493,6 +2504,7 @@ export class AccountDto implements IAccountDto { data["username"] = this.username; data["email"] = this.email; data["avatarPath"] = this.avatarPath; + data["theme"] = this.theme; return data; } } @@ -2502,12 +2514,14 @@ export interface IAccountDto { username?: string; email?: string; avatarPath?: string; + theme?: InterfaceTheme; } export class EditUserModel implements IEditUserModel { username: string; email: string; avatar?: string; + theme?: InterfaceTheme; constructor(data?: IEditUserModel) { if (data) { @@ -2523,6 +2537,7 @@ export class EditUserModel implements IEditUserModel { this.username = _data["username"]; this.email = _data["email"]; this.avatar = _data["avatar"]; + this.theme = _data["theme"]; } } @@ -2538,6 +2553,7 @@ export class EditUserModel implements IEditUserModel { data["username"] = this.username; data["email"] = this.email; data["avatar"] = this.avatar; + data["theme"] = this.theme; return data; } } @@ -2546,6 +2562,7 @@ export interface IEditUserModel { username: string; email: string; avatar?: string; + theme?: InterfaceTheme; } export class ChangePasswordModel implements IChangePasswordModel { diff --git a/web/src/lib/components/ContextMenuEntry.svelte b/web/src/lib/components/ContextMenuEntry.svelte index e871187..ba8128c 100644 --- a/web/src/lib/components/ContextMenuEntry.svelte +++ b/web/src/lib/components/ContextMenuEntry.svelte @@ -34,5 +34,5 @@ height: 1.5em &:hover - background-color: var(--background-secondary-hover) + background-color: var(--background-hover) \ No newline at end of file diff --git a/web/src/lib/components/IconButton.svelte b/web/src/lib/components/IconButton.svelte index b4d3015..45e9383 100644 --- a/web/src/lib/components/IconButton.svelte +++ b/web/src/lib/components/IconButton.svelte @@ -26,6 +26,7 @@ border: 0 padding: 0.3em 0.4em border-radius: var(--radius) + color: var(--on-background) font-family: inherit font-weight: 500 cursor: pointer diff --git a/web/src/lib/components/editor/editor.css b/web/src/lib/components/editor/editor.css index 577b998..7e16595 100644 --- a/web/src/lib/components/editor/editor.css +++ b/web/src/lib/components/editor/editor.css @@ -35,6 +35,12 @@ body { border-top-right-radius: var(--radius); } +.editor-shell .toolbar-item i, .editor-shell .icon { + background-color: var(--on-background); + mask-repeat: no-repeat; + mask-size: cover; +} + .editor-scroller { min-height: 150px; max-height: 30em; @@ -127,7 +133,8 @@ pre::-webkit-scrollbar-thumb { } #paste-log-button::after { - background-image: url(./icons/clipboard.svg); + mask-image: url(./icons/clipboard.svg); + -webkit-mask-image: url(./icons/clipboard.svg); } .component-picker-menu { @@ -147,137 +154,170 @@ pre::-webkit-scrollbar-thumb { } i.palette { - background-image: url(./icons/palette.svg); + mask-image: url(./icons/palette.svg); + -webkit-mask-image: url(./icons/palette.svg); } i.bucket { - background-image: url(./icons/paint-bucket.svg); + mask-image: url(./icons/paint-bucket.svg); + -webkit-mask-image: url(./icons/paint-bucket.svg); } i.bold { - background-image: url(./icons/type-bold.svg); + mask-image: url(./icons/type-bold.svg); + -webkit-mask-image: url(./icons/type-bold.svg); } i.italic { - background-image: url(./icons/type-italic.svg); + mask-image: url(./icons/type-italic.svg); + -webkit-mask-image: url(./icons/type-italic.svg); } i.clear { - background-image: url(./icons/trash.svg); + mask-image: url(./icons/trash.svg); + -webkit-mask-image: url(./icons/trash.svg); } i.code { - background-image: url(./icons/code.svg); + mask-image: url(./icons/code.svg); + -webkit-mask-image: url(./icons/code.svg); } i.underline { - background-image: url(./icons/type-underline.svg); + mask-image: url(./icons/type-underline.svg); + -webkit-mask-image: url(./icons/type-underline.svg); } i.strikethrough { - background-image: url(./icons/type-strikethrough.svg); + mask-image: url(./icons/type-strikethrough.svg); + -webkit-mask-image: url(./icons/type-strikethrough.svg); } i.subscript { - background-image: url(./icons/type-subscript.svg); + mask-image: url(./icons/type-subscript.svg); + -webkit-mask-image: url(./icons/type-subscript.svg); } i.superscript { - background-image: url(./icons/type-superscript.svg); + mask-image: url(./icons/type-superscript.svg); + -webkit-mask-image: url(./icons/type-superscript.svg); } i.link { - background-image: url(./icons/link.svg); + mask-image: url(./icons/link.svg); + -webkit-mask-image: url(./icons/link.svg); } i.horizontal-rule { - background-image: url(./icons/horizontal-rule.svg); + mask-image: url(./icons/horizontal-rule.svg); + -webkit-mask-image: url(./icons/horizontal-rule.svg); } .icon.plus { - background-image: url(./icons/plus.svg); + mask-image: url(./icons/plus.svg); + -webkit-mask-image: url(./icons/plus.svg); } .icon.caret-right { - background-image: url(./icons/caret-right-fill.svg); + mask-image: url(./icons/caret-right-fill.svg); + -webkit-mask-image: url(./icons/caret-right-fill.svg); } .icon.dropdown-more { - background-image: url(./icons/dropdown-more.svg); + mask-image: url(./icons/dropdown-more.svg); + -webkit-mask-image: url(./icons/dropdown-more.svg); } .icon.font-color { - background-image: url(./icons/font-color.svg); + mask-image: url(./icons/font-color.svg); + -webkit-mask-image: url(./icons/font-color.svg); } .icon.font-family { - background-image: url(./icons/font-family.svg); + mask-image: url(./icons/font-family.svg); + -webkit-mask-image: url(./icons/font-family.svg); } .icon.bg-color { - background-image: url(./icons/bg-color.svg); + mask-image: url(./icons/bg-color.svg); + -webkit-mask-image: url(./icons/bg-color.svg); } i.image { - background-image: url(./icons/file-image.svg); + mask-image: url(./icons/file-image.svg); + -webkit-mask-image: url(./icons/file-image.svg); } i.table { - background-image: url(./icons/table.svg); + mask-image: url(./icons/table.svg); + -webkit-mask-image: url(./icons/table.svg); } i.close { - background-image: url(./icons/close.svg); + mask-image: url(./icons/close.svg); + -webkit-mask-image: url(./icons/close.svg); } .icon.left-align, i.left-align { - background-image: url(./icons/text-left.svg); + mask-image: url(./icons/text-left.svg); + -webkit-mask-image: url(./icons/text-left.svg); } i.center-align { - background-image: url(./icons/text-center.svg); + mask-image: url(./icons/text-center.svg); + -webkit-mask-image: url(./icons/text-center.svg); } i.right-align { - background-image: url(./icons/text-right.svg); + mask-image: url(./icons/text-right.svg); + -webkit-mask-image: url(./icons/text-right.svg); } i.justify-align { - background-image: url(./icons/justify.svg); + mask-image: url(./icons/justify.svg); + -webkit-mask-image: url(./icons/justify.svg); } i.indent { - background-image: url(./icons/indent.svg); + mask-image: url(./icons/indent.svg); + -webkit-mask-image: url(./icons/indent.svg); } i.markdown { - background-image: url(./icons/markdown.svg); + mask-image: url(./icons/markdown.svg); + -webkit-mask-image: url(./icons/markdown.svg); } i.outdent { - background-image: url(./icons/outdent.svg); + mask-image: url(./icons/outdent.svg); + -webkit-mask-image: url(./icons/outdent.svg); } i.undo { - background-image: url(./icons/arrow-counterclockwise.svg); + mask-image: url(./icons/arrow-counterclockwise.svg); + -webkit-mask-image: url(./icons/arrow-counterclockwise.svg); } i.redo { - background-image: url(./icons/arrow-clockwise.svg); + mask-image: url(./icons/arrow-clockwise.svg); + -webkit-mask-image: url(./icons/arrow-clockwise.svg); } i.gif { - background-image: url(./icons/filetype-gif.svg); + mask-image: url(./icons/filetype-gif.svg); + -webkit-mask-image: url(./icons/filetype-gif.svg); } i.copy { - background-image: url(./icons/copy.svg); + mask-image: url(./icons/copy.svg); + -webkit-mask-image: url(./icons/copy.svg); } i.success { - background-image: url(./icons/success.svg); + mask-image: url(./icons/success.svg); + -webkit-mask-image: url(./icons/success.svg); } .link-editor .button.active, @@ -314,7 +354,8 @@ i.success { } .link-editor div.link-edit { - background-image: url(./icons/pencil-square.svg); + mask-image: url(./icons/pencil-square.svg); + -webkit-mask-image: url(./icons/pencil-square.svg); background-size: 1.25em; background-position: center; background-repeat: no-repeat; @@ -404,31 +445,38 @@ select.font-family { } #block-controls span.block-type.paragraph { - background-image: url(./icons/text-paragraph.svg); + mask-image: url(./icons/text-paragraph.svg); + -webkit-mask-image: url(./icons/text-paragraph.svg); } #block-controls span.block-type.h1 { - background-image: url(./icons/type-h1.svg); + mask-image: url(./icons/type-h1.svg); + -webkit-mask-image: url(./icons/type-h1.svg); } #block-controls span.block-type.h2 { - background-image: url(./icons/type-h2.svg); + mask-image: url(./icons/type-h2.svg); + -webkit-mask-image: url(./icons/type-h2.svg); } #block-controls span.block-type.quote { - background-image: url(./icons/chat-square-quote.svg); + mask-image: url(./icons/chat-square-quote.svg); + -webkit-mask-image: url(./icons/chat-square-quote.svg); } #block-controls span.block-type.ul { - background-image: url(./icons/list-ul.svg); + mask-image: url(./icons/list-ul.svg); + -webkit-mask-image: url(./icons/list-ul.svg); } #block-controls span.block-type.ol { - background-image: url(./icons/list-ol.svg); + mask-image: url(./icons/list-ol.svg); + -webkit-mask-image: url(./icons/list-ol.svg); } #block-controls span.block-type.code { - background-image: url(./icons/code.svg); + mask-image: url(./icons/code.svg); + -webkit-mask-image: url(./icons/code.svg); } .characters-limit { @@ -527,54 +575,66 @@ select.font-family { } .icon.paragraph { - background-image: url(./icons/text-paragraph.svg); + mask-image: url(./icons/text-paragraph.svg); + -webkit-mask-image: url(./icons/text-paragraph.svg); } .icon.h1 { - background-image: url(./icons/type-h1.svg); + mask-image: url(./icons/type-h1.svg); + -webkit-mask-image: url(./icons/type-h1.svg); } .icon.h2 { - background-image: url(./icons/type-h2.svg); + mask-image: url(./icons/type-h2.svg); + -webkit-mask-image: url(./icons/type-h2.svg); } .icon.h3 { - background-image: url(./icons/type-h3.svg); + mask-image: url(./icons/type-h3.svg); + -webkit-mask-image: url(./icons/type-h3.svg); } .icon.h4 { - background-image: url(./icons/type-h4.svg); + mask-image: url(./icons/type-h4.svg); + -webkit-mask-image: url(./icons/type-h4.svg); } .icon.h5 { - background-image: url(./icons/type-h5.svg); + mask-image: url(./icons/type-h5.svg); + -webkit-mask-image: url(./icons/type-h5.svg); } .icon.h6 { - background-image: url(./icons/type-h6.svg); + mask-image: url(./icons/type-h6.svg); + -webkit-mask-image: url(./icons/type-h6.svg); } .icon.bullet-list, .icon.bullet { - background-image: url(./icons/list-ul.svg); + mask-image: url(./icons/list-ul.svg); + -webkit-mask-image: url(./icons/list-ul.svg); } .icon.check-list, .icon.check { - background-image: url(./icons/square-check.svg); + mask-image: url(./icons/square-check.svg); + -webkit-mask-image: url(./icons/square-check.svg); } .icon.numbered-list, .icon.number { - background-image: url(./icons/list-ol.svg); + mask-image: url(./icons/list-ol.svg); + -webkit-mask-image: url(./icons/list-ol.svg); } .icon.quote { - background-image: url(./icons/chat-square-quote.svg); + mask-image: url(./icons/chat-square-quote.svg); + -webkit-mask-image: url(./icons/chat-square-quote.svg); } .icon.code { - background-image: url(./icons/code.svg); + mask-image: url(./icons/code.svg); + -webkit-mask-image: url(./icons/code.svg); } .switches { @@ -812,51 +872,63 @@ select.font-family { } .actions i.indent { - background-image: url(./icons/indent.svg); + mask-image: url(./icons/indent.svg); + -webkit-mask-image: url(./icons/indent.svg); } .actions i.outdent { - background-image: url(./icons/outdent.svg); + mask-image: url(./icons/outdent.svg); + -webkit-mask-image: url(./icons/outdent.svg); } .actions i.lock { - background-image: url(./icons/lock-fill.svg); + mask-image: url(./icons/lock-fill.svg); + -webkit-mask-image: url(./icons/lock-fill.svg); } .actions i.image { - background-image: url(./icons/file-image.svg); + mask-image: url(./icons/file-image.svg); + -webkit-mask-image: url(./icons/file-image.svg); } .actions i.table { - background-image: url(./icons/table.svg); + mask-image: url(./icons/table.svg); + -webkit-mask-image: url(./icons/table.svg); } .actions i.unlock { - background-image: url(./icons/lock.svg); + mask-image: url(./icons/lock.svg); + -webkit-mask-image: url(./icons/lock.svg); } .actions i.left-align { - background-image: url(./icons/text-left.svg); + mask-image: url(./icons/text-left.svg); + -webkit-mask-image: url(./icons/text-left.svg); } .actions i.center-align { - background-image: url(./icons/text-center.svg); + mask-image: url(./icons/text-center.svg); + -webkit-mask-image: url(./icons/text-center.svg); } .actions i.right-align { - background-image: url(./icons/text-right.svg); + mask-image: url(./icons/text-right.svg); + -webkit-mask-image: url(./icons/text-right.svg); } .actions i.justify-align { - background-image: url(./icons/justify.svg); + mask-image: url(./icons/justify.svg); + -webkit-mask-image: url(./icons/justify.svg); } .actions i.disconnect { - background-image: url(./icons/plug.svg); + mask-image: url(./icons/plug.svg); + -webkit-mask-image: url(./icons/plug.svg); } .actions i.connect { - background-image: url(./icons/plug-fill.svg); + mask-image: url(./icons/plug-fill.svg); + -webkit-mask-image: url(./icons/plug-fill.svg); } table.disable-selection { @@ -900,7 +972,8 @@ i.chevron-down { display: inline-block; height: 8px; width: 8px; - background-image: url(./icons/chevron-down.svg); + mask-image: url(./icons/chevron-down.svg); + -webkit-mask-image: url(./icons/chevron-down.svg); } .action-button { diff --git a/web/src/lib/components/editor/ticketEditorTheme.ts b/web/src/lib/components/editor/ticketEditorTheme.ts index 2e8e576..5cc713e 100644 --- a/web/src/lib/components/editor/ticketEditorTheme.ts +++ b/web/src/lib/components/editor/ticketEditorTheme.ts @@ -1,106 +1,104 @@ -import type {EditorThemeClasses} from 'svelte-lexical'; +import type {EditorThemeClasses} from "@paddim8/svelte-lexical"; -import './ticketEditorTheme.css'; +import "./ticketEditorTheme.css"; -const theme: EditorThemeClasses = { - blockCursor: 'TicketEditorTheme__blockCursor', - characterLimit: 'TicketEditorTheme__characterLimit', - code: 'TicketEditorTheme__code', +export default { + blockCursor: "TicketEditorTheme__blockCursor", + characterLimit: "TicketEditorTheme__characterLimit", + code: "TicketEditorTheme__code", codeHighlight: { - atrule: 'TicketEditorTheme__tokenAttr', - attr: 'TicketEditorTheme__tokenAttr', - boolean: 'TicketEditorTheme__tokenProperty', - builtin: 'TicketEditorTheme__tokenSelector', - cdata: 'TicketEditorTheme__tokenComment', - char: 'TicketEditorTheme__tokenSelector', - class: 'TicketEditorTheme__tokenFunction', - 'class-name': 'TicketEditorTheme__tokenFunction', - comment: 'TicketEditorTheme__tokenComment', - constant: 'TicketEditorTheme__tokenProperty', - deleted: 'TicketEditorTheme__tokenProperty', - doctype: 'TicketEditorTheme__tokenComment', - entity: 'TicketEditorTheme__tokenOperator', - function: 'TicketEditorTheme__tokenFunction', - important: 'TicketEditorTheme__tokenVariable', - inserted: 'TicketEditorTheme__tokenSelector', - keyword: 'TicketEditorTheme__tokenAttr', - namespace: 'TicketEditorTheme__tokenVariable', - number: 'TicketEditorTheme__tokenProperty', - operator: 'TicketEditorTheme__tokenOperator', - prolog: 'TicketEditorTheme__tokenComment', - property: 'TicketEditorTheme__tokenProperty', - punctuation: 'TicketEditorTheme__tokenPunctuation', - regex: 'TicketEditorTheme__tokenVariable', - selector: 'TicketEditorTheme__tokenSelector', - string: 'TicketEditorTheme__tokenSelector', - symbol: 'TicketEditorTheme__tokenProperty', - tag: 'TicketEditorTheme__tokenProperty', - url: 'TicketEditorTheme__tokenOperator', - variable: 'TicketEditorTheme__tokenVariable', + atrule: "TicketEditorTheme__tokenAttr", + attr: "TicketEditorTheme__tokenAttr", + boolean: "TicketEditorTheme__tokenProperty", + builtin: "TicketEditorTheme__tokenSelector", + cdata: "TicketEditorTheme__tokenComment", + char: "TicketEditorTheme__tokenSelector", + class: "TicketEditorTheme__tokenFunction", + "class-name": "TicketEditorTheme__tokenFunction", + comment: "TicketEditorTheme__tokenComment", + constant: "TicketEditorTheme__tokenProperty", + deleted: "TicketEditorTheme__tokenProperty", + doctype: "TicketEditorTheme__tokenComment", + entity: "TicketEditorTheme__tokenOperator", + function: "TicketEditorTheme__tokenFunction", + important: "TicketEditorTheme__tokenVariable", + inserted: "TicketEditorTheme__tokenSelector", + keyword: "TicketEditorTheme__tokenAttr", + namespace: "TicketEditorTheme__tokenVariable", + number: "TicketEditorTheme__tokenProperty", + operator: "TicketEditorTheme__tokenOperator", + prolog: "TicketEditorTheme__tokenComment", + property: "TicketEditorTheme__tokenProperty", + punctuation: "TicketEditorTheme__tokenPunctuation", + regex: "TicketEditorTheme__tokenVariable", + selector: "TicketEditorTheme__tokenSelector", + string: "TicketEditorTheme__tokenSelector", + symbol: "TicketEditorTheme__tokenProperty", + tag: "TicketEditorTheme__tokenProperty", + url: "TicketEditorTheme__tokenOperator", + variable: "TicketEditorTheme__tokenVariable", }, embedBlock: { - base: 'TicketEditorTheme__embedBlock', - focus: 'TicketEditorTheme__embedBlockFocus', + base: "TicketEditorTheme__embedBlock", + focus: "TicketEditorTheme__embedBlockFocus", }, - hashtag: 'TicketEditorTheme__hashtag', + hashtag: "TicketEditorTheme__hashtag", heading: { - h1: 'TicketEditorTheme__h1', - h2: 'TicketEditorTheme__h2', - h3: 'TicketEditorTheme__h3', - h4: 'TicketEditorTheme__h4', - h5: 'TicketEditorTheme__h5', - h6: 'TicketEditorTheme__h6', + h1: "TicketEditorTheme__h1", + h2: "TicketEditorTheme__h2", + h3: "TicketEditorTheme__h3", + h4: "TicketEditorTheme__h4", + h5: "TicketEditorTheme__h5", + h6: "TicketEditorTheme__h6", }, - image: 'editor-image', - indent: 'TicketEditorTheme__indent', - link: 'TicketEditorTheme__link', + image: "editor-image", + indent: "TicketEditorTheme__indent", + link: "TicketEditorTheme__link", list: { - listitem: 'TicketEditorTheme__listItem', - listitemChecked: 'TicketEditorTheme__listItemChecked', - listitemUnchecked: 'TicketEditorTheme__listItemUnchecked', + listitem: "TicketEditorTheme__listItem", + listitemChecked: "TicketEditorTheme__listItemChecked", + listitemUnchecked: "TicketEditorTheme__listItemUnchecked", nested: { - listitem: 'TicketEditorTheme__nestedListItem', + listitem: "TicketEditorTheme__nestedListItem", }, olDepth: [ - 'TicketEditorTheme__ol1', - 'TicketEditorTheme__ol2', - 'TicketEditorTheme__ol3', - 'TicketEditorTheme__ol4', - 'TicketEditorTheme__ol5', + "TicketEditorTheme__ol1", + "TicketEditorTheme__ol2", + "TicketEditorTheme__ol3", + "TicketEditorTheme__ol4", + "TicketEditorTheme__ol5", ], - ul: 'TicketEditorTheme__ul', + ul: "TicketEditorTheme__ul", }, - ltr: 'TicketEditorTheme__ltr', - mark: 'TicketEditorTheme__mark', - markOverlap: 'TicketEditorTheme__markOverlap', - paragraph: 'TicketEditorTheme__paragraph', - quote: 'TicketEditorTheme__quote', - rtl: 'TicketEditorTheme__rtl', - table: 'TicketEditorTheme__table', - tableAddColumns: 'TicketEditorTheme__tableAddColumns', - tableAddRows: 'TicketEditorTheme__tableAddRows', - tableCell: 'TicketEditorTheme__tableCell', - tableCellActionButton: 'TicketEditorTheme__tableCellActionButton', + ltr: "TicketEditorTheme__ltr", + mark: "TicketEditorTheme__mark", + markOverlap: "TicketEditorTheme__markOverlap", + paragraph: "TicketEditorTheme__paragraph", + quote: "TicketEditorTheme__quote", + rtl: "TicketEditorTheme__rtl", + table: "TicketEditorTheme__table", + tableAddColumns: "TicketEditorTheme__tableAddColumns", + tableAddRows: "TicketEditorTheme__tableAddRows", + tableCell: "TicketEditorTheme__tableCell", + tableCellActionButton: "TicketEditorTheme__tableCellActionButton", tableCellActionButtonContainer: - 'TicketEditorTheme__tableCellActionButtonContainer', - tableCellEditing: 'TicketEditorTheme__tableCellEditing', - tableCellHeader: 'TicketEditorTheme__tableCellHeader', - tableCellPrimarySelected: 'TicketEditorTheme__tableCellPrimarySelected', - tableCellResizer: 'TicketEditorTheme__tableCellResizer', - tableCellSelected: 'TicketEditorTheme__tableCellSelected', - tableCellSortedIndicator: 'TicketEditorTheme__tableCellSortedIndicator', - tableResizeRuler: 'TicketEditorTheme__tableCellResizeRuler', - tableSelected: 'TicketEditorTheme__tableSelected', + "TicketEditorTheme__tableCellActionButtonContainer", + tableCellEditing: "TicketEditorTheme__tableCellEditing", + tableCellHeader: "TicketEditorTheme__tableCellHeader", + tableCellPrimarySelected: "TicketEditorTheme__tableCellPrimarySelected", + tableCellResizer: "TicketEditorTheme__tableCellResizer", + tableCellSelected: "TicketEditorTheme__tableCellSelected", + tableCellSortedIndicator: "TicketEditorTheme__tableCellSortedIndicator", + tableResizeRuler: "TicketEditorTheme__tableCellResizeRuler", + tableSelected: "TicketEditorTheme__tableSelected", text: { - bold: 'TicketEditorTheme__textBold', - code: 'TicketEditorTheme__textCode', - italic: 'TicketEditorTheme__textItalic', - strikethrough: 'TicketEditorTheme__textStrikethrough', - subscript: 'TicketEditorTheme__textSubscript', - superscript: 'TicketEditorTheme__textSuperscript', - underline: 'TicketEditorTheme__textUnderline', - underlineStrikethrough: 'TicketEditorTheme__textUnderlineStrikethrough', + bold: "TicketEditorTheme__textBold", + code: "TicketEditorTheme__textCode", + italic: "TicketEditorTheme__textItalic", + strikethrough: "TicketEditorTheme__textStrikethrough", + subscript: "TicketEditorTheme__textSubscript", + superscript: "TicketEditorTheme__textSuperscript", + underline: "TicketEditorTheme__textUnderline", + underlineStrikethrough: "TicketEditorTheme__textUnderlineStrikethrough", }, -}; - -export default theme; \ No newline at end of file +} as EditorThemeClasses; \ No newline at end of file diff --git a/web/src/lib/components/form/BlockInput.svelte b/web/src/lib/components/form/BlockInput.svelte index ff997b9..b0a79e5 100644 --- a/web/src/lib/components/form/BlockInput.svelte +++ b/web/src/lib/components/form/BlockInput.svelte @@ -25,7 +25,7 @@ let suggestionList: SuggestionList; const dispatcher = createEventDispatcher(); - function getValue(obj) { + function getValue(obj: any) { return key ? obj[key] : obj; @@ -191,7 +191,7 @@ transform: translateY(-50%) width: calc(100% - 1em) height: 2px - background-color: black + background-color: var(--on-background) .icon width: 1.2em @@ -210,6 +210,7 @@ padding: calc(var(--vertical-padding) / 2) calc(var(--horizontal-padding) / 2) border: 0 min-width: 5em + color: var(--on-background) background-color: transparent box-sizing: border-box diff --git a/web/src/lib/components/form/Input.svelte b/web/src/lib/components/form/Input.svelte index a7a9e87..ec86dc3 100644 --- a/web/src/lib/components/form/Input.svelte +++ b/web/src/lib/components/form/Input.svelte @@ -67,6 +67,7 @@ padding: var(--vertical-padding) var(--horizontal-padding) border-radius: var(--radius) border: 0 + color: var(--on-background) background-color: var(--component-background) outline: var(--border) box-sizing: border-box diff --git a/web/src/lib/components/form/Select.svelte b/web/src/lib/components/form/Select.svelte index fba2859..42702b6 100644 --- a/web/src/lib/components/form/Select.svelte +++ b/web/src/lib/components/form/Select.svelte @@ -69,6 +69,8 @@ padding: var(--vertical-padding) var(--horizontal-padding) padding-right: 2em border-radius: var(--radius) + color: var(--on-background) + font-weight: 500 border: 0 background-color: var(--component-background) outline: var(--border) diff --git a/web/src/lib/components/form/SuggestionList.svelte b/web/src/lib/components/form/SuggestionList.svelte index 4e8184a..434dd56 100644 --- a/web/src/lib/components/form/SuggestionList.svelte +++ b/web/src/lib/components/form/SuggestionList.svelte @@ -12,11 +12,10 @@ export let selectedValue: any | undefined = undefined; export let ignored: any[] = []; - let shownItems = []; - let previousIndex = 0; + let shownItems: any[] = []; const dispatch = createEventDispatcher(); - function getValue(obj) { + function getValue(obj: any) { return key ? obj[key] : obj; @@ -29,17 +28,14 @@ } $: { - //if (previousIndex == selectedIndex) { - const indexedItems = items.map((x, i) => { - return { ...x, index: i }; - }); - - shownItems = indexedItems.filter(x => - !ignored.some(y => y[key] === x[key]) && getValue(x).includes(query) - ); - selectedIndex = 0; - //previousIndex = selectedIndex; - //} + const indexedItems = items.map((x, i) => { + return { ...x, index: i }; + }); + + shownItems = indexedItems.filter(x => + !ignored.some(y => y[key ?? ""] === x[key ?? ""]) && getValue(x).includes(query) + ); + selectedIndex = 0; } export function selectNext() { @@ -55,7 +51,7 @@ // components may want to listen to on:blur to know // when the close the suggestion list, but the suggestion // list should get a chance to listen for clicks first. - function handleItemClick(index) { + function handleItemClick(index: number) { query = ""; dispatch("select", { value: shownItems[index], diff --git a/web/src/lib/components/sidebar/SidebarEntry.svelte b/web/src/lib/components/sidebar/SidebarEntry.svelte index 73ab239..ae62bf5 100644 --- a/web/src/lib/components/sidebar/SidebarEntry.svelte +++ b/web/src/lib/components/sidebar/SidebarEntry.svelte @@ -44,7 +44,7 @@ margin-top: 0.2em border-radius: var(--radius) - color: black + color: var(--on-background) text-decoration: none font-weight: 425 cursor: pointer diff --git a/web/src/lib/components/ticket/TicketEntry.svelte b/web/src/lib/components/ticket/TicketEntry.svelte index d668f6c..8d55044 100644 --- a/web/src/lib/components/ticket/TicketEntry.svelte +++ b/web/src/lib/components/ticket/TicketEntry.svelte @@ -41,6 +41,7 @@ ); toast.info("Added assignee successfully."); + ticket.assignees = [...ticket.assignees!]; } catch { toast.error("Failed to add assignee."); ticket.assignees = ticket.assignees?.filter(x => x.id !== $user.id); @@ -157,7 +158,7 @@ - {#each ticket.assignees ?? [] as assignee} + {#each ticket.assignees as assignee} x[1] === key); if (matchedEntries.length === 0) { @@ -11,4 +13,4 @@ export function truncate(value: string, length: number) { return value.length > length ? value.slice(0, length).trim() + "..." : value; -} \ No newline at end of file +} diff --git a/web/src/routes/(auth)/logout/+page.server.ts b/web/src/routes/(auth)/logout/+page.server.ts index 8437f56..cee3166 100644 --- a/web/src/routes/(auth)/logout/+page.server.ts +++ b/web/src/routes/(auth)/logout/+page.server.ts @@ -3,7 +3,7 @@ import {getAuthenticationClient} from "$lib/clients"; export async function load({ cookies }: ServerLoadEvent) { await getAuthenticationClient(cookies).logout(); - cookies.delete("token"); + cookies.delete("token", { path: "/" }); throw redirect(302, "/login"); } \ No newline at end of file diff --git a/web/src/routes/(main)/invitations/+page.svelte b/web/src/routes/(main)/invitations/+page.svelte index a8d090e..5b2f63c 100644 --- a/web/src/routes/(main)/invitations/+page.svelte +++ b/web/src/routes/(main)/invitations/+page.svelte @@ -1,5 +1,5 @@ @@ -17,9 +30,13 @@ -