From aae12a56900cff2d3aa5280288345faf0e2250a4 Mon Sep 17 00:00:00 2001 From: Lee Houk Date: Mon, 17 Mar 2025 14:50:10 -0700 Subject: [PATCH] add read-only assetTagNames property to Asset interface Adds a read-only property to Asset interface that returns tag names as strings. Creates utility functions for processing assets with tag name getters. Updates DataService to process assets with tag names. Updates Project Page component to use assetTagNames property for filtering. --- ...20250317212011_modifiedTagData.Designer.cs | 667 ++++++++++++++++++ .../20250317212011_modifiedTagData.cs | 174 +++++ .../ModelCabinetContextModelSnapshot.cs | 208 +++++- modelcabinet.client/src/app/Models/asset.ts | 8 +- modelcabinet.client/src/app/asset-utils.ts | 41 ++ modelcabinet.client/src/app/data.service.ts | 53 +- .../project-page/project-page.component.html | 106 +-- .../project-page/project-page.component.ts | 61 +- 8 files changed, 1252 insertions(+), 66 deletions(-) create mode 100644 ModelCabinet.Server/Migrations/20250317212011_modifiedTagData.Designer.cs create mode 100644 ModelCabinet.Server/Migrations/20250317212011_modifiedTagData.cs create mode 100644 modelcabinet.client/src/app/asset-utils.ts diff --git a/ModelCabinet.Server/Migrations/20250317212011_modifiedTagData.Designer.cs b/ModelCabinet.Server/Migrations/20250317212011_modifiedTagData.Designer.cs new file mode 100644 index 0000000..c26ade2 --- /dev/null +++ b/ModelCabinet.Server/Migrations/20250317212011_modifiedTagData.Designer.cs @@ -0,0 +1,667 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ModelCabinet.Server.Data; + +#nullable disable + +namespace ModelCabinet.Server.Migrations +{ + [DbContext(typeof(ModelCabinetContext))] + [Migration("20250317212011_modifiedTagData")] + partial class modifiedTagData + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("ModelCabinet.Server.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Biography") + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("DateJoined") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("EmailNotificationsEnabled") + .HasColumnType("bit"); + + b.Property("GithubUsername") + .HasColumnType("nvarchar(max)"); + + b.Property("IsProfilePublic") + .HasColumnType("bit"); + + b.Property("LastActive") + .HasColumnType("datetime2"); + + b.Property("Location") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NewMessageNotificationsEnabled") + .HasColumnType("bit"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("PreferredLanguage") + .HasColumnType("nvarchar(max)"); + + b.Property("ProjectUpdatesEnabled") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TimeZone") + .HasColumnType("nvarchar(max)"); + + b.Property("TwitterHandle") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Website") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("ModelCabinet.Server.Models.Asset", b => + { + b.Property("AssetId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("AssetId")); + + b.Property("DateCreation") + .HasColumnType("datetime2"); + + b.Property("DateUpdated") + .HasColumnType("datetime2"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProjectId") + .HasColumnType("int"); + + b.HasKey("AssetId"); + + b.HasIndex("ProjectId"); + + b.ToTable("Asset"); + + b.HasData( + new + { + AssetId = 1, + DateCreation = new DateTime(2025, 3, 17, 14, 20, 10, 930, DateTimeKind.Local).AddTicks(2109), + DateUpdated = new DateTime(2025, 3, 17, 14, 20, 10, 930, DateTimeKind.Local).AddTicks(2164), + FileSize = 446684L, + Name = "Test Asset", + Path = "Assets\\TestProject\\HelloWorld.stl", + ProjectId = 1 + }, + new + { + AssetId = 2, + DateCreation = new DateTime(2025, 3, 17, 14, 20, 10, 930, DateTimeKind.Local).AddTicks(2169), + DateUpdated = new DateTime(2025, 3, 17, 14, 20, 10, 930, DateTimeKind.Local).AddTicks(2171), + FileSize = 11285384L, + Name = "Benchy", + Path = "Assets\\TestProject\\3DBenchy.stl", + ProjectId = 1 + }); + }); + + modelBuilder.Entity("ModelCabinet.Server.Models.AssetTag", b => + { + b.Property("AssetId") + .HasColumnType("int"); + + b.Property("TagId") + .HasColumnType("int"); + + b.HasKey("AssetId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("AssetTag"); + + b.HasData( + new + { + AssetId = 2, + TagId = 1 + }, + new + { + AssetId = 2, + TagId = 5 + }, + new + { + AssetId = 2, + TagId = 2 + }, + new + { + AssetId = 1, + TagId = 4 + }, + new + { + AssetId = 1, + TagId = 6 + }); + }); + + modelBuilder.Entity("ModelCabinet.Server.Models.Project", b => + { + b.Property("ProjectId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ProjectId")); + + b.Property("Author") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Version") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("ProjectId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Project"); + + b.HasData( + new + { + ProjectId = 1, + Author = "Author", + CreationDate = new DateTime(2024, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Description", + ModifiedDate = new DateTime(2024, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Name = "Test Project", + ShortDescription = "Desc", + Slug = "nomen-est-omen", + Version = "0.0.1" + }, + new + { + ProjectId = 2, + Author = "Author", + CreationDate = new DateTime(2024, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Description", + ModifiedDate = new DateTime(2024, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Name = "Test Project Two", + ShortDescription = "Desc", + Slug = "nomen-est-bonum", + Version = "0.0.1" + }); + }); + + modelBuilder.Entity("ModelCabinet.Server.Models.ProjectTag", b => + { + b.Property("ProjectId") + .HasColumnType("int"); + + b.Property("TagId") + .HasColumnType("int"); + + b.HasKey("ProjectId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("ProjectTag"); + + b.HasData( + new + { + ProjectId = 1, + TagId = 3 + }, + new + { + ProjectId = 1, + TagId = 4 + }, + new + { + ProjectId = 1, + TagId = 5 + }, + new + { + ProjectId = 2, + TagId = 1 + }, + new + { + ProjectId = 2, + TagId = 2 + }, + new + { + ProjectId = 2, + TagId = 5 + }); + }); + + modelBuilder.Entity("ModelCabinet.Server.Models.Tag", b => + { + b.Property("TagId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("TagId")); + + b.Property("Color") + .IsRequired() + .HasColumnType("char(6)"); + + b.Property("TagName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("TagId"); + + b.HasIndex("TagName") + .IsUnique(); + + b.ToTable("Tag"); + + b.HasData( + new + { + TagId = 1, + Color = "fae033", + TagName = "Stress Test" + }, + new + { + TagId = 2, + Color = "df0000", + TagName = "D&D" + }, + new + { + TagId = 3, + Color = "40E0D0", + TagName = "Pathfinder" + }, + new + { + TagId = 4, + Color = "afafaf", + TagName = "Low Detail" + }, + new + { + TagId = 5, + Color = "3f3f3f", + TagName = "High Detail" + }, + new + { + TagId = 6, + Color = "23a300", + TagName = "Video Game" + }); + }); + + 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("ModelCabinet.Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("ModelCabinet.Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ModelCabinet.Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("ModelCabinet.Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ModelCabinet.Server.Models.Asset", b => + { + b.HasOne("ModelCabinet.Server.Models.Project", null) + .WithMany("Assets") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ModelCabinet.Server.Models.AssetTag", b => + { + b.HasOne("ModelCabinet.Server.Models.Asset", "Asset") + .WithMany("AssetTags") + .HasForeignKey("AssetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ModelCabinet.Server.Models.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Asset"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("ModelCabinet.Server.Models.ProjectTag", b => + { + b.HasOne("ModelCabinet.Server.Models.Project", "Project") + .WithMany("ProjectTags") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ModelCabinet.Server.Models.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("ModelCabinet.Server.Models.Asset", b => + { + b.Navigation("AssetTags"); + }); + + modelBuilder.Entity("ModelCabinet.Server.Models.Project", b => + { + b.Navigation("Assets"); + + b.Navigation("ProjectTags"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ModelCabinet.Server/Migrations/20250317212011_modifiedTagData.cs b/ModelCabinet.Server/Migrations/20250317212011_modifiedTagData.cs new file mode 100644 index 0000000..57a4434 --- /dev/null +++ b/ModelCabinet.Server/Migrations/20250317212011_modifiedTagData.cs @@ -0,0 +1,174 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace ModelCabinet.Server.Migrations +{ + /// + public partial class modifiedTagData : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Tag", + columns: table => new + { + TagId = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + TagName = table.Column(type: "nvarchar(450)", nullable: false), + Color = table.Column(type: "char(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tag", x => x.TagId); + }); + + migrationBuilder.CreateTable( + name: "AssetTag", + columns: table => new + { + AssetId = table.Column(type: "int", nullable: false), + TagId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AssetTag", x => new { x.AssetId, x.TagId }); + table.ForeignKey( + name: "FK_AssetTag_Asset_AssetId", + column: x => x.AssetId, + principalTable: "Asset", + principalColumn: "AssetId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AssetTag_Tag_TagId", + column: x => x.TagId, + principalTable: "Tag", + principalColumn: "TagId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ProjectTag", + columns: table => new + { + ProjectId = table.Column(type: "int", nullable: false), + TagId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProjectTag", x => new { x.ProjectId, x.TagId }); + table.ForeignKey( + name: "FK_ProjectTag_Project_ProjectId", + column: x => x.ProjectId, + principalTable: "Project", + principalColumn: "ProjectId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ProjectTag_Tag_TagId", + column: x => x.TagId, + principalTable: "Tag", + principalColumn: "TagId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.UpdateData( + table: "Asset", + keyColumn: "AssetId", + keyValue: 1, + columns: new[] { "DateCreation", "DateUpdated" }, + values: new object[] { new DateTime(2025, 3, 17, 14, 20, 10, 930, DateTimeKind.Local).AddTicks(2109), new DateTime(2025, 3, 17, 14, 20, 10, 930, DateTimeKind.Local).AddTicks(2164) }); + + migrationBuilder.UpdateData( + table: "Asset", + keyColumn: "AssetId", + keyValue: 2, + columns: new[] { "DateCreation", "DateUpdated" }, + values: new object[] { new DateTime(2025, 3, 17, 14, 20, 10, 930, DateTimeKind.Local).AddTicks(2169), new DateTime(2025, 3, 17, 14, 20, 10, 930, DateTimeKind.Local).AddTicks(2171) }); + + migrationBuilder.InsertData( + table: "Tag", + columns: new[] { "TagId", "Color", "TagName" }, + values: new object[,] + { + { 1, "fae033", "Stress Test" }, + { 2, "df0000", "D&D" }, + { 3, "40E0D0", "Pathfinder" }, + { 4, "afafaf", "Low Detail" }, + { 5, "3f3f3f", "High Detail" }, + { 6, "23a300", "Video Game" } + }); + + migrationBuilder.InsertData( + table: "AssetTag", + columns: new[] { "AssetId", "TagId" }, + values: new object[,] + { + { 1, 4 }, + { 1, 6 }, + { 2, 1 }, + { 2, 2 }, + { 2, 5 } + }); + + migrationBuilder.InsertData( + table: "ProjectTag", + columns: new[] { "ProjectId", "TagId" }, + values: new object[,] + { + { 1, 3 }, + { 1, 4 }, + { 1, 5 }, + { 2, 1 }, + { 2, 2 }, + { 2, 5 } + }); + + migrationBuilder.CreateIndex( + name: "IX_AssetTag_TagId", + table: "AssetTag", + column: "TagId"); + + migrationBuilder.CreateIndex( + name: "IX_ProjectTag_TagId", + table: "ProjectTag", + column: "TagId"); + + migrationBuilder.CreateIndex( + name: "IX_Tag_TagName", + table: "Tag", + column: "TagName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AssetTag"); + + migrationBuilder.DropTable( + name: "ProjectTag"); + + migrationBuilder.DropTable( + name: "Tag"); + + migrationBuilder.UpdateData( + table: "Asset", + keyColumn: "AssetId", + keyValue: 1, + columns: new[] { "DateCreation", "DateUpdated" }, + values: new object[] { new DateTime(2025, 2, 25, 14, 39, 3, 727, DateTimeKind.Local).AddTicks(5697), new DateTime(2025, 2, 25, 14, 39, 3, 727, DateTimeKind.Local).AddTicks(5756) }); + + migrationBuilder.UpdateData( + table: "Asset", + keyColumn: "AssetId", + keyValue: 2, + columns: new[] { "DateCreation", "DateUpdated" }, + values: new object[] { new DateTime(2025, 2, 25, 14, 39, 3, 727, DateTimeKind.Local).AddTicks(5760), new DateTime(2025, 2, 25, 14, 39, 3, 727, DateTimeKind.Local).AddTicks(5761) }); + } + } +} diff --git a/ModelCabinet.Server/Migrations/ModelCabinetContextModelSnapshot.cs b/ModelCabinet.Server/Migrations/ModelCabinetContextModelSnapshot.cs index e63828b..689cd9b 100644 --- a/ModelCabinet.Server/Migrations/ModelCabinetContextModelSnapshot.cs +++ b/ModelCabinet.Server/Migrations/ModelCabinetContextModelSnapshot.cs @@ -301,14 +301,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ProjectId"); - b.ToTable("Asset", (string)null); + b.ToTable("Asset"); b.HasData( new { AssetId = 1, - DateCreation = new DateTime(2025, 2, 25, 14, 39, 3, 727, DateTimeKind.Local).AddTicks(5697), - DateUpdated = new DateTime(2025, 2, 25, 14, 39, 3, 727, DateTimeKind.Local).AddTicks(5756), + DateCreation = new DateTime(2025, 3, 17, 14, 20, 10, 930, DateTimeKind.Local).AddTicks(2109), + DateUpdated = new DateTime(2025, 3, 17, 14, 20, 10, 930, DateTimeKind.Local).AddTicks(2164), FileSize = 446684L, Name = "Test Asset", Path = "Assets\\TestProject\\HelloWorld.stl", @@ -317,8 +317,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) new { AssetId = 2, - DateCreation = new DateTime(2025, 2, 25, 14, 39, 3, 727, DateTimeKind.Local).AddTicks(5760), - DateUpdated = new DateTime(2025, 2, 25, 14, 39, 3, 727, DateTimeKind.Local).AddTicks(5761), + DateCreation = new DateTime(2025, 3, 17, 14, 20, 10, 930, DateTimeKind.Local).AddTicks(2169), + DateUpdated = new DateTime(2025, 3, 17, 14, 20, 10, 930, DateTimeKind.Local).AddTicks(2171), FileSize = 11285384L, Name = "Benchy", Path = "Assets\\TestProject\\3DBenchy.stl", @@ -326,6 +326,48 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("ModelCabinet.Server.Models.AssetTag", b => + { + b.Property("AssetId") + .HasColumnType("int"); + + b.Property("TagId") + .HasColumnType("int"); + + b.HasKey("AssetId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("AssetTag"); + + b.HasData( + new + { + AssetId = 2, + TagId = 1 + }, + new + { + AssetId = 2, + TagId = 5 + }, + new + { + AssetId = 2, + TagId = 2 + }, + new + { + AssetId = 1, + TagId = 4 + }, + new + { + AssetId = 1, + TagId = 6 + }); + }); + modelBuilder.Entity("ModelCabinet.Server.Models.Project", b => { b.Property("ProjectId") @@ -369,7 +411,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("Slug") .IsUnique(); - b.ToTable("Project", (string)null); + b.ToTable("Project"); b.HasData( new @@ -398,6 +440,115 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("ModelCabinet.Server.Models.ProjectTag", b => + { + b.Property("ProjectId") + .HasColumnType("int"); + + b.Property("TagId") + .HasColumnType("int"); + + b.HasKey("ProjectId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("ProjectTag"); + + b.HasData( + new + { + ProjectId = 1, + TagId = 3 + }, + new + { + ProjectId = 1, + TagId = 4 + }, + new + { + ProjectId = 1, + TagId = 5 + }, + new + { + ProjectId = 2, + TagId = 1 + }, + new + { + ProjectId = 2, + TagId = 2 + }, + new + { + ProjectId = 2, + TagId = 5 + }); + }); + + modelBuilder.Entity("ModelCabinet.Server.Models.Tag", b => + { + b.Property("TagId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("TagId")); + + b.Property("Color") + .IsRequired() + .HasColumnType("char(6)"); + + b.Property("TagName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("TagId"); + + b.HasIndex("TagName") + .IsUnique(); + + b.ToTable("Tag"); + + b.HasData( + new + { + TagId = 1, + Color = "fae033", + TagName = "Stress Test" + }, + new + { + TagId = 2, + Color = "df0000", + TagName = "D&D" + }, + new + { + TagId = 3, + Color = "40E0D0", + TagName = "Pathfinder" + }, + new + { + TagId = 4, + Color = "afafaf", + TagName = "Low Detail" + }, + new + { + TagId = 5, + Color = "3f3f3f", + TagName = "High Detail" + }, + new + { + TagId = 6, + Color = "23a300", + TagName = "Video Game" + }); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) @@ -458,9 +609,54 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("ModelCabinet.Server.Models.AssetTag", b => + { + b.HasOne("ModelCabinet.Server.Models.Asset", "Asset") + .WithMany("AssetTags") + .HasForeignKey("AssetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ModelCabinet.Server.Models.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Asset"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("ModelCabinet.Server.Models.ProjectTag", b => + { + b.HasOne("ModelCabinet.Server.Models.Project", "Project") + .WithMany("ProjectTags") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ModelCabinet.Server.Models.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("ModelCabinet.Server.Models.Asset", b => + { + b.Navigation("AssetTags"); + }); + modelBuilder.Entity("ModelCabinet.Server.Models.Project", b => { b.Navigation("Assets"); + + b.Navigation("ProjectTags"); }); #pragma warning restore 612, 618 } diff --git a/modelcabinet.client/src/app/Models/asset.ts b/modelcabinet.client/src/app/Models/asset.ts index a7f4b5f..10e0fa8 100644 --- a/modelcabinet.client/src/app/Models/asset.ts +++ b/modelcabinet.client/src/app/Models/asset.ts @@ -9,6 +9,9 @@ export interface Asset { fileSize: number, projectId: number, assetTags: AssetTag[] + + // Read-only property + readonly assetTagNames: string[] } // Used to Match the structure in the backend @@ -27,5 +30,6 @@ export const emptyAsset: Asset = { dateUpdated: new Date("2025-03-10"), fileSize: 0, projectId: 0, - assetTags: [] -} \ No newline at end of file + assetTags: [], + get assetTagNames() { return []; } +} diff --git a/modelcabinet.client/src/app/asset-utils.ts b/modelcabinet.client/src/app/asset-utils.ts new file mode 100644 index 0000000..1ff87de --- /dev/null +++ b/modelcabinet.client/src/app/asset-utils.ts @@ -0,0 +1,41 @@ +import { Asset } from './Models/asset'; + +/** + * Returns an array of tag names from an asset's assetTags array + * @param asset The asset to extract tag names from + * @returns string[] Array of tag names + */ +export function getAssetTagNames(asset: Asset): string[] { + if (!asset.assetTags || asset.assetTags.length === 0) { + return []; + } + + return asset.assetTags.map(assetTag => assetTag.tag.tagName); +} + +/** + * Creates a new Asset object with assetTagNames getter implemented + * @param asset Original asset object from API + * @returns Asset with assetTagNames getter implemented + */ +export function createAssetWithTagNames(asset: Asset): Asset { + // Create a new object with everything from the original asset + const processedAsset = { + ...asset, + // Define getter for assetTagNames + get assetTagNames(): string[] { + return getAssetTagNames(this); + } + }; + + return processedAsset; +} + +/** + * Process multiple assets to add assetTagNames getters + * @param assets Array of assets from API + * @returns Array of assets with assetTagNames getters + */ +export function processAssetsWithTagNames(assets: Asset[]): Asset[] { + return assets.map(asset => createAssetWithTagNames(asset)); +} diff --git a/modelcabinet.client/src/app/data.service.ts b/modelcabinet.client/src/app/data.service.ts index f7d1a73..49e44c3 100644 --- a/modelcabinet.client/src/app/data.service.ts +++ b/modelcabinet.client/src/app/data.service.ts @@ -4,6 +4,7 @@ import { BehaviorSubject, Observable, tap } from "rxjs"; import { emptyProject, Project } from "./Models/project"; import { Asset, emptyAsset } from "./Models/asset"; import { emptyTag, Tag } from "./Models/tag"; +import { createAssetWithTagNames, processAssetsWithTagNames } from "./asset-utils"; //import { Tag } from "./Models/tag"; @@ -57,9 +58,19 @@ export class DataService { } getProjectById(id: number) { - this.http.get(`/api/Projects/${id}`).subscribe(data => { - this.project$.next(data); - // this.assets$.next(data.asset.&values); + this.http.get(`/api/Projects/${id}`).subscribe({ + next: (data) => { + // Process assets in the project + if (data.assets && data.assets.length > 0) { + data.assets = processAssetsWithTagNames(data.assets); + } + console.log("Project loaded successfully:", data); + this.project$.next(data); + }, + error: (err) => { + console.error("Error loading project:", err); + // Don't update the BehaviorSubject with invalid data + } }); } @@ -93,31 +104,40 @@ export class DataService { createAsset(asset: Asset) { this.http.post(`/api/Assets`, asset).subscribe(data => { - this.asset$.next(data); + const processedAsset = createAssetWithTagNames(data); + this.asset$.next(processedAsset); }); } getAllAssets() { this.http.get(`/api/Assets`).subscribe(data => { - this.assets$.next(data); + // Process assets to add the assetTagNames property + const processedAssets = processAssetsWithTagNames(data); + this.assets$.next(processedAssets); }); } getAssetsById(id: number) { this.http.get(`/api/Assets/${id}`).subscribe(data => { - this.asset$.next(data); - }) + // Add the assetTagNames property + const processedAsset = createAssetWithTagNames(data); + this.asset$.next(processedAsset); + }); } - updateAssetById(id:number, asset: Asset) { + updateAssetById(id: number, asset: Asset) { this.http.put(`/api/Assets/${id}`, asset).subscribe(data => { - this.asset$.next(data); + const processedAsset = createAssetWithTagNames(data); + this.asset$.next(processedAsset); }); } deleteAssetById(id: number) { this.http.delete(`/api/Assets/${id}`).subscribe(data => { - this.asset$.next(data); + if (data) { + const processedAsset = createAssetWithTagNames(data); + this.asset$.next(processedAsset); + } }); } @@ -157,4 +177,17 @@ export class DataService { return data; }); } + + getAllUniqueTags(): string[] { + const assets = this.assets$.value; + const allTags = new Set(); + + assets.forEach(asset => { + if (asset.assetTagNames) { + asset.assetTagNames.forEach(tag => allTags.add(tag)); + } + }); + + return Array.from(allTags).sort(); + } } diff --git a/modelcabinet.client/src/app/projects/project-page/project-page.component.html b/modelcabinet.client/src/app/projects/project-page/project-page.component.html index 1fbab51..4d50dc9 100644 --- a/modelcabinet.client/src/app/projects/project-page/project-page.component.html +++ b/modelcabinet.client/src/app/projects/project-page/project-page.component.html @@ -1,37 +1,46 @@ -@let project = project$ | async; + +
+
+
+ Loading... +
+
+

Loading project data...

+
-
+ +
- +
-

{{ project!.name }}

+

{{ project$.value.name }}

Description:

-

{{ project!.shortDescription }}

+

{{ project$.value.shortDescription }}

-

Author:
{{ project!.author }}

+

Author:
{{ project$.value.author }}

-

Creation Date:
{{ project!.creationDate | date:'short' }}

+

Creation Date:
{{ project$.value.creationDate | date:'short' }}

-

Modified Date:
{{ project!.modifiedDate | date:'short' }}

+

Modified Date:
{{ project$.value.modifiedDate | date:'short' }}

-
+
    -
  • +
  • {{ asset.name }}
  • @@ -39,54 +48,63 @@

    {{ project!.name }}

-
+
Tags:
- + +
+
+
+ + +
+
+
+
Filter Assets by Tag
+
+
+ +
+
+ +
+
-
Assets Included In Project:
-
+
Assets Included In Project:
+
-
+
+ +
+ No assets match the selected tag filter. +
+
+ + +
+
+ This project has no assets. +
- -