diff --git a/src/Analysim.Core/Entities/Project.cs b/src/Analysim.Core/Entities/Project.cs index 06013a5d..1cbed8b2 100644 --- a/src/Analysim.Core/Entities/Project.cs +++ b/src/Analysim.Core/Entities/Project.cs @@ -45,8 +45,8 @@ public class Project // Logs public ICollection ProjectLogs { get; set; } = new List(); public ICollection Publications { get; set; } = new List(); + public ICollection ProjectMembershipRequests { get; set; } = new List(); public int ForkedFromProjectID { get; set; } } } - diff --git a/src/Analysim.Core/Entities/ProjectMembershipRequest.cs b/src/Analysim.Core/Entities/ProjectMembershipRequest.cs new file mode 100644 index 00000000..3765e424 --- /dev/null +++ b/src/Analysim.Core/Entities/ProjectMembershipRequest.cs @@ -0,0 +1,46 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Core.Entities +{ + public class ProjectMembershipRequest + { + [Key] + public int ProjectMembershipRequestID { get; set; } + + [Required(ErrorMessage = "Project ID is a required field.")] + [ForeignKey("Project")] + public int ProjectID { get; set; } + public Project Project { get; set; } + + [Required(ErrorMessage = "Requester User ID is a required field.")] + [ForeignKey("RequesterUser")] + public int RequesterUserID { get; set; } + public User RequesterUser { get; set; } + + [Required(ErrorMessage = "Target User ID is a required field.")] + [ForeignKey("TargetUser")] + public int TargetUserID { get; set; } + public User TargetUser { get; set; } + + [Required(ErrorMessage = "Created By User ID is a required field.")] + [ForeignKey("CreatedByUser")] + public int CreatedByUserID { get; set; } + public User CreatedByUser { get; set; } + + [Required(ErrorMessage = "Request Type is a required field.")] + public string Type { get; set; } + + [Required(ErrorMessage = "Request Status is a required field.")] + public string Status { get; set; } + + [MaxLength(500, ErrorMessage = "Maximum length for Message is 500 Character.")] + public string Message { get; set; } + + [Required(ErrorMessage = "Created At is a required field.")] + public DateTimeOffset CreatedAt { get; set; } + + public DateTimeOffset? RespondedAt { get; set; } + } +} diff --git a/src/Analysim.Infrastructure/Data/ApplicationDbContext.cs b/src/Analysim.Infrastructure/Data/ApplicationDbContext.cs index 79751278..3de7857e 100644 --- a/src/Analysim.Infrastructure/Data/ApplicationDbContext.cs +++ b/src/Analysim.Infrastructure/Data/ApplicationDbContext.cs @@ -258,6 +258,37 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .WithOne(pp => pp.Project) .HasForeignKey(pp => pp.ProjectID) .OnDelete(DeleteBehavior.Cascade); + + // One To Many Relationship (Project -> ProjectMembershipRequest) + modelBuilder.Entity() + .HasMany(p => p.ProjectMembershipRequests) + .WithOne(pmr => pmr.Project) + .HasForeignKey(pmr => pmr.ProjectID) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasOne(pmr => pmr.RequesterUser) + .WithMany() + .HasForeignKey(pmr => pmr.RequesterUserID) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(pmr => pmr.TargetUser) + .WithMany() + .HasForeignKey(pmr => pmr.TargetUserID) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(pmr => pmr.CreatedByUser) + .WithMany() + .HasForeignKey(pmr => pmr.CreatedByUserID) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasIndex(pmr => new { pmr.ProjectID, pmr.TargetUserID, pmr.Type, pmr.Status }); + + modelBuilder.Entity() + .HasIndex(pmr => pmr.CreatedByUserID); } @@ -279,5 +310,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) public DbSet ProjectLogs { get; set; } public DbSet Publications { get; set; } + public DbSet ProjectMembershipRequests { get; set; } } } diff --git a/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 91c60939..815f97a9 100644 --- a/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -414,6 +414,57 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ProjectTags"); }); + modelBuilder.Entity("Core.Entities.ProjectMembershipRequest", b => + { + b.Property("ProjectMembershipRequestID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ProjectMembershipRequestID")); + + b.Property("CreatedByUserID") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("RespondedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RequesterUserID") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetUserID") + .HasColumnType("integer"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ProjectMembershipRequestID"); + + b.HasIndex("CreatedByUserID"); + + b.HasIndex("RequesterUserID"); + + b.HasIndex("TargetUserID"); + + b.HasIndex("ProjectID", "TargetUserID", "Type", "Status"); + + b.ToTable("ProjectMembershipRequests"); + }); + modelBuilder.Entity("Core.Entities.ProjectUser", b => { b.Property("UserID") @@ -1019,6 +1070,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Core.Entities.ProjectMembershipRequest", b => + { + b.HasOne("Core.Entities.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserID") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Core.Entities.Project", "Project") + .WithMany("ProjectMembershipRequests") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "RequesterUser") + .WithMany() + .HasForeignKey("RequesterUserID") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Core.Entities.User", "TargetUser") + .WithMany() + .HasForeignKey("TargetUserID") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Project"); + + b.Navigation("RequesterUser"); + + b.Navigation("TargetUser"); + }); + modelBuilder.Entity("Core.Entities.Publication", b => { b.HasOne("Core.Entities.Project", "Project") @@ -1133,6 +1219,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("ProjectLogs"); + b.Navigation("ProjectMembershipRequests"); + b.Navigation("ProjectTags"); b.Navigation("ProjectUsers"); diff --git a/src/Analysim.Infrastructure/Migrations/20260830040500_AddProjectMembershipRequests.cs b/src/Analysim.Infrastructure/Migrations/20260830040500_AddProjectMembershipRequests.cs new file mode 100644 index 00000000..9fd53cdd --- /dev/null +++ b/src/Analysim.Infrastructure/Migrations/20260830040500_AddProjectMembershipRequests.cs @@ -0,0 +1,89 @@ +using System; +using Infrastructure.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260830040500_AddProjectMembershipRequests")] + public partial class AddProjectMembershipRequests : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ProjectMembershipRequests", + columns: table => new + { + ProjectMembershipRequestID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectID = table.Column(type: "integer", nullable: false), + RequesterUserID = table.Column(type: "integer", nullable: false), + TargetUserID = table.Column(type: "integer", nullable: false), + CreatedByUserID = table.Column(type: "integer", nullable: false), + Type = table.Column(type: "text", nullable: false), + Status = table.Column(type: "text", nullable: false), + Message = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + RespondedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ProjectMembershipRequests", x => x.ProjectMembershipRequestID); + table.ForeignKey( + name: "FK_ProjectMembershipRequests_AspNetUsers_CreatedByUserID", + column: x => x.CreatedByUserID, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ProjectMembershipRequests_AspNetUsers_RequesterUserID", + column: x => x.RequesterUserID, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ProjectMembershipRequests_AspNetUsers_TargetUserID", + column: x => x.TargetUserID, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ProjectMembershipRequests_Projects_ProjectID", + column: x => x.ProjectID, + principalTable: "Projects", + principalColumn: "ProjectID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ProjectMembershipRequests_CreatedByUserID", + table: "ProjectMembershipRequests", + column: "CreatedByUserID"); + + migrationBuilder.CreateIndex( + name: "IX_ProjectMembershipRequests_ProjectID_TargetUserID_Type_Status", + table: "ProjectMembershipRequests", + columns: new[] { "ProjectID", "TargetUserID", "Type", "Status" }); + + migrationBuilder.CreateIndex( + name: "IX_ProjectMembershipRequests_RequesterUserID", + table: "ProjectMembershipRequests", + column: "RequesterUserID"); + + migrationBuilder.CreateIndex( + name: "IX_ProjectMembershipRequests_TargetUserID", + table: "ProjectMembershipRequests", + column: "TargetUserID"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ProjectMembershipRequests"); + } + } +} diff --git a/src/Analysim.Web/ClientApp/src/app/interfaces/project-membership-request.ts b/src/Analysim.Web/ClientApp/src/app/interfaces/project-membership-request.ts new file mode 100644 index 00000000..20f82e2a --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/interfaces/project-membership-request.ts @@ -0,0 +1,19 @@ +import { Project } from './project'; +import { User } from './user'; + +export interface ProjectMembershipRequest { + projectMembershipRequestID: number + projectID: number + project: Project + requesterUserID: number + requesterUser: User + targetUserID: number + targetUser: User + createdByUserID: number + createdByUser: User + type: string + status: string + message: string + createdAt: Date + respondedAt: Date +} diff --git a/src/Analysim.Web/ClientApp/src/app/profile/profile.component.html b/src/Analysim.Web/ClientApp/src/app/profile/profile.component.html index 3f75fa90..d1a2d2e2 100644 --- a/src/Analysim.Web/ClientApp/src/app/profile/profile.component.html +++ b/src/Analysim.Web/ClientApp/src/app/profile/profile.component.html @@ -71,6 +71,15 @@
{{ profile.userName }}
+ + @@ -97,6 +106,45 @@
{{ profile.userName }}
+ + @@ -106,4 +154,4 @@
{{ profile.userName }}
- \ No newline at end of file + diff --git a/src/Analysim.Web/ClientApp/src/app/profile/profile.component.scss b/src/Analysim.Web/ClientApp/src/app/profile/profile.component.scss index dceeb4de..db07220d 100644 --- a/src/Analysim.Web/ClientApp/src/app/profile/profile.component.scss +++ b/src/Analysim.Web/ClientApp/src/app/profile/profile.component.scss @@ -20,4 +20,43 @@ .nav-item:hover { cursor: pointer; -} \ No newline at end of file +} + +.profile-invite-row { + align-items: center; + border: 1px solid #dee2e6; + display: flex; + gap: 12px; + justify-content: space-between; + padding: 14px; +} + +.profile-invite-main { + min-width: 0; +} + +.profile-invite-title { + font-weight: 500; +} + +.profile-invite-title a { + color: var(--link-on-light); +} + +.profile-invite-actions { + display: flex; + flex-shrink: 0; + gap: 8px; +} + +.profile-invite-actions .btn-outline-danger { + background-color: transparent; + color: #dc3545; +} + +@media (max-width: 576px) { + .profile-invite-row { + align-items: flex-start; + flex-direction: column; + } +} diff --git a/src/Analysim.Web/ClientApp/src/app/profile/profile.component.ts b/src/Analysim.Web/ClientApp/src/app/profile/profile.component.ts index 1a4330ba..9a5206c9 100644 --- a/src/Analysim.Web/ClientApp/src/app/profile/profile.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/profile/profile.component.ts @@ -4,10 +4,10 @@ import { AccountService } from '../services/account.service'; import { User } from '../interfaces/user'; import { Project } from '../interfaces/project'; import { ProjectService } from '../services/project.service'; -import { of, pipe, Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; -import { UserUser } from '../interfaces/user-user'; +import { Observable } from 'rxjs'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { ProjectMembershipRequest } from '../interfaces/project-membership-request'; +import { ProjectUser } from '../interfaces/project-user'; @Component({ selector: 'app-profile', @@ -29,9 +29,11 @@ export class ProfileComponent implements OnInit { projects: Project[] followings: User[] followers: User[] + membershipRequests: ProjectMembershipRequest[] = [] - tabActive: boolean[] = [true, false, false] + tabActive: boolean[] = [true, false, false, false] showError: boolean + requestActionID: number = null currentUser: User = null @@ -39,7 +41,10 @@ export class ProfileComponent implements OnInit { async ngOnInit() { if (this.accountService.checkLoginStatus()) { await this.accountService.currentUser.then((x) => this.currentUser$ = x) - this.currentUser$.subscribe(x => this.currentUser = x) + this.currentUser$.subscribe(x => { + this.currentUser = x + this.loadMembershipRequests() + }) } this.route.params.subscribe(params => { @@ -47,7 +52,10 @@ export class ProfileComponent implements OnInit { this.projects = [] this.followings = [] this.followers = [] + this.membershipRequests = [] this.showError = false + this.requestActionID = null + this.tabActive = [true, false, false, false] let username = params["username"] @@ -58,6 +66,7 @@ export class ProfileComponent implements OnInit { this.loadProject(result) this.loadFollowing(result) this.loadFollower(result) + this.loadMembershipRequests() this.profileImage() }, error => { this.showError = true @@ -86,6 +95,8 @@ export class ProfileComponent implements OnInit { } changeTab(num: number) { + if (num == 3 && !this.isOwnProfile) return + this.tabActive.forEach((t, i) => { if (num != i) this.tabActive[i] = false @@ -126,6 +137,92 @@ export class ProfileComponent implements OnInit { ) } + get isOwnProfile(): boolean { + return this.profile != null && + this.currentUser != null && + this.profile.id == this.currentUser.id + } + + get pendingInvitations(): ProjectMembershipRequest[] { + if (this.currentUser == null) return [] + + return this.membershipRequests.filter(request => + request.type == "invitation" && + request.status == "pending" && + request.targetUserID == this.currentUser.id) + } + + loadMembershipRequests() { + if (!this.isOwnProfile) return + + this.projectService.getMyMembershipRequests().subscribe( + result => { + this.membershipRequests = result.filter(request => + request.type == "invitation" && + request.status == "pending" && + request.targetUserID == this.currentUser.id) + }, error => { + console.log(error) + } + ) + } + + acceptInvitation(request: ProjectMembershipRequest) { + if (this.requestActionID != null) return + this.requestActionID = request.projectMembershipRequestID + + this.projectService.acceptProjectMembershipRequest(request.projectMembershipRequestID).subscribe( + result => { + this.removeMembershipRequest(request.projectMembershipRequestID) + this.addProjectUser(result) + this.loadAcceptedProject(result) + }, error => { + console.log(error) + this.requestActionID = null + } + ) + } + + rejectInvitation(request: ProjectMembershipRequest) { + if (this.requestActionID != null) return + this.requestActionID = request.projectMembershipRequestID + + this.projectService.rejectProjectMembershipRequest(request.projectMembershipRequestID).subscribe( + result => { + this.removeMembershipRequest(result.projectMembershipRequestID) + this.requestActionID = null + }, error => { + console.log(error) + this.requestActionID = null + } + ) + } + + private removeMembershipRequest(requestID: number) { + this.membershipRequests = this.membershipRequests + .filter(request => request.projectMembershipRequestID != requestID) + } + + private addProjectUser(projectUser: ProjectUser) { + let index = this.profile.projectUsers.findIndex(item => item.projectID == projectUser.projectID) + if(index > -1) this.profile.projectUsers[index] = projectUser + else this.profile.projectUsers.push(projectUser) + } + + private loadAcceptedProject(projectUser: ProjectUser) { + this.projectService.getProjectByID(projectUser.projectID).subscribe( + project => { + let index = this.projects.findIndex(item => item.projectID == project.projectID) + if(index > -1) this.projects[index] = project + else this.projects.push(project) + this.requestActionID = null + }, error => { + console.log(error) + this.requestActionID = null + } + ) + } + loadProject(profile: User) { let projectIDs: number[] = profile.projectUsers.map(pu => pu.projectID) diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-create.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-create.component.html index d064ddb3..d16cc416 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-create.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-create.component.html @@ -20,8 +20,12 @@

Project Creation

- - + + + diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-create.component.ts b/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-create.component.ts index 6a03fa87..058ed82e 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-create.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-create.component.ts @@ -5,6 +5,7 @@ import { Router } from '@angular/router'; import { Project } from 'src/app/interfaces/project'; import { ProjectUser } from 'src/app/interfaces/project-user'; import { ProjectTag } from 'src/app/interfaces/project-tag'; +import { ProjectMembershipRequest } from 'src/app/interfaces/project-membership-request'; @Component({ selector: 'app-project-create', @@ -14,6 +15,7 @@ import { ProjectTag } from 'src/app/interfaces/project-tag'; export class ProjectCreateComponent implements OnInit { project : Project = null + membershipRequests : ProjectMembershipRequest[] = [] constructor(private projectService : ProjectService, private formBuilder : FormBuilder, @@ -36,4 +38,8 @@ export class ProjectCreateComponent implements OnInit { this.project.projectUsers = projectUsers } + addMembershipRequest(request : ProjectMembershipRequest){ + this.membershipRequests.push(request) + } + } diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-users/project-form-users.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-users/project-form-users.component.html index 40aa8b7b..d2d26a80 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-users/project-form-users.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-users/project-form-users.component.html @@ -5,13 +5,13 @@
+ {{invitationMessage}} -
\ No newline at end of file +
diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-users/project-form-users.component.ts b/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-users/project-form-users.component.ts index 322c7ab7..ed06e182 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-users/project-form-users.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-users/project-form-users.component.ts @@ -4,6 +4,7 @@ import { FormGroup, FormControl, FormBuilder } from '@angular/forms'; import { ProjectService } from 'src/app/services/project.service'; import { User } from 'src/app/interfaces/user'; import { AccountService } from 'src/app/services/account.service'; +import { ProjectMembershipRequest } from 'src/app/interfaces/project-membership-request'; @Component({ selector: 'app-project-form-users', @@ -19,11 +20,13 @@ export class ProjectFormUsersComponent implements OnInit { // Form Control - Project Tag/Role @Input() project : Project - @Output() updateProject = new EventEmitter() + @Input() membershipRequests : ProjectMembershipRequest[] = [] + @Output() invitationCreated = new EventEmitter() users : User[] = [] filteredUsers : User[] = [] userForm: FormGroup userName: FormControl + invitationMessage: string = '' ngOnInit(): void { this.loadUser() @@ -35,12 +38,11 @@ export class ProjectFormUsersComponent implements OnInit { } ) - // Initialize FormGroup using FormBuilder + this.userName = new FormControl(''); this.userForm = this.formBuilder.group({ userName: this.userName, }); - this.userName = new FormControl(''); this.userForm.get("userName").valueChanges.subscribe( val => { this.filteredUsers =this.filterUser(val) @@ -53,17 +55,23 @@ export class ProjectFormUsersComponent implements OnInit { if(val == null || val == "") return [] - var existingUser : User[] = [] + var existingUserIDs : number[] = [] this.project.projectUsers .forEach(x => { if(x.userRole != "follower"){ - existingUser.push(x.user) + existingUserIDs.push(x.userID) } }) return this.users.filter(x => { if(count < 5){ - if(x.userName.toLowerCase().indexOf(val.toLowerCase()) != -1 && existingUser.find(u => x.userName == u.userName) == undefined){ + let hasPendingMembershipRequest = this.membershipRequests.find(request => + request.targetUserID == x.id && + request.status == "pending") != undefined + + if(x.userName.toLowerCase().indexOf(val.toLowerCase()) != -1 && + existingUserIDs.find(userID => x.id == userID) == undefined && + !hasPendingMembershipRequest){ count++ return true } @@ -76,13 +84,16 @@ export class ProjectFormUsersComponent implements OnInit { } - public addUser(user : User){ - this.projectService.addUser(this.project.projectID, user.id, "member", false).subscribe( + public inviteUser(user : User){ + this.invitationMessage = '' + + this.projectService.inviteProjectMember(this.project.projectID, user.id).subscribe( result => { - this.project.projectUsers.push(result) - this.updateProject.emit(this.project) + this.invitationCreated.emit(result) + this.invitationMessage = "Invitation sent to " + user.userName }, error =>{ console.log(error) + this.invitationMessage = error?.error?.message || "Failed to send invitation." } ) this.userForm.reset() @@ -96,9 +107,6 @@ export class ProjectFormUsersComponent implements OnInit { // Map Project in Project User this.project.projectUsers.map(pu => pu.user = result.find(u => u.id == pu.userID)) - // Map Project - this.users = this.project.projectUsers - .map(x => x.user) }, error =>{ console.log(error) } diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-edit.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-edit.component.html index 330efdf0..e092e010 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-edit.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-edit.component.html @@ -14,19 +14,81 @@ (setProject)="setProject($event)"> -
-
-
Project Tags
- +
+
Project Tags
+ - -
+ (updateProjectTags)="updateProjectTags($event)"> + + +
+
Project Members
+ + + + + +
+
Join Requests
+
    +
  • +
    +
    + {{request.requesterUser?.userName}} + {{request.createdAt | date:'medium'}} +
    +
    + + +
    +
    +
  • +
+

No pending join requests.

+ +
Pending Invitations
+
    +
  • +
    +
    + {{request.targetUser?.userName}} + {{request.createdAt | date:'medium'}} +
    + +
    +
  • +
+

No pending invitations.

+ diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-edit.component.scss b/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-edit.component.scss index e69de29b..9fdd0956 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-edit.component.scss +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-edit.component.scss @@ -0,0 +1,19 @@ +.membership-request-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.membership-request-actions { + display: flex; + align-items: center; + gap: 8px; +} + +@media (max-width: 576px) { + .membership-request-row { + align-items: flex-start; + flex-direction: column; + } +} diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-edit.component.ts b/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-edit.component.ts index a11c7a19..6c5d945d 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-edit.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-edit.component.ts @@ -1,8 +1,10 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Project } from 'src/app/interfaces/project'; -import { ProjectUser } from 'src/app/interfaces/project-user'; import { ProjectTag } from 'src/app/interfaces/project-tag'; +import { ProjectUser } from 'src/app/interfaces/project-user'; +import { ProjectMembershipRequest } from 'src/app/interfaces/project-membership-request'; +import { ProjectService } from 'src/app/services/project.service'; @Component({ selector: 'app-project-edit', @@ -14,8 +16,13 @@ export class ProjectEditComponent implements OnInit { project : Project = null; owner: string = null; projectname: string = null; + membershipRequests: ProjectMembershipRequest[] = []; + requestActionID: number = null; - constructor(private route: ActivatedRoute) { } + constructor( + private route: ActivatedRoute, + private projectService: ProjectService + ) { } ngOnInit(): void { @@ -27,6 +34,7 @@ export class ProjectEditComponent implements OnInit { setProject(project : Project){ this.project = project + this.loadMembershipRequests() } updateProjectTags(projectTags : ProjectTag[]){ @@ -37,4 +45,77 @@ export class ProjectEditComponent implements OnInit { this.project.projectUsers = projectUsers } + get pendingJoinRequests(): ProjectMembershipRequest[] { + return this.membershipRequests.filter(request => + request.type == "join_request" && + request.status == "pending") + } + + get pendingInvitations(): ProjectMembershipRequest[] { + return this.membershipRequests.filter(request => + request.type == "invitation" && + request.status == "pending") + } + + loadMembershipRequests(){ + if(this.project == null) return + + this.projectService.getProjectMembershipRequests(this.project.projectID).subscribe( + result => { + this.membershipRequests = result + }, error => { + console.log(error) + } + ) + } + + acceptRequest(request: ProjectMembershipRequest){ + if(this.requestActionID != null) return + this.requestActionID = request.projectMembershipRequestID + + this.projectService.acceptProjectMembershipRequest(request.projectMembershipRequestID).subscribe( + result => { + let index = this.project.projectUsers.findIndex(projectUser => projectUser.userID == result.userID) + if(index > -1) this.project.projectUsers[index] = result + else this.project.projectUsers.push(result) + + this.membershipRequests = this.membershipRequests.filter(item => item.projectMembershipRequestID != request.projectMembershipRequestID) + this.requestActionID = null + }, error => { + console.log(error) + this.requestActionID = null + } + ) + } + + rejectRequest(request: ProjectMembershipRequest){ + if(this.requestActionID != null) return + this.requestActionID = request.projectMembershipRequestID + + this.projectService.rejectProjectMembershipRequest(request.projectMembershipRequestID).subscribe( + result => { + this.membershipRequests = this.membershipRequests.filter(item => item.projectMembershipRequestID != result.projectMembershipRequestID) + this.requestActionID = null + }, error => { + console.log(error) + this.requestActionID = null + } + ) + } + + cancelInvitation(request: ProjectMembershipRequest){ + if(this.requestActionID != null) return + this.requestActionID = request.projectMembershipRequestID + + this.projectService.cancelProjectMembershipRequest(request.projectMembershipRequestID).subscribe( + result => { + this.membershipRequests = this.membershipRequests.filter(item => item.projectMembershipRequestID != result.projectMembershipRequestID) + this.requestActionID = null + }, error => { + console.log(error) + this.requestActionID = null + } + ) + } + } diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-list-users.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-list-users.component.html index 344d9a77..c41964ab 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-list-users.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-list-users.component.html @@ -1,9 +1,9 @@ -
+
    - +
-
\ No newline at end of file +
diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-list-users.component.ts b/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-list-users.component.ts index f33e7e48..25b28cdb 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-list-users.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-list-users.component.ts @@ -11,24 +11,11 @@ export class ProjectListUsersComponent implements OnInit { constructor() { } @Input() projectUsers : ProjectUser[] - @Output() updateProjectUsers = new EventEmitter() @Output() removeProjectUsers = new EventEmitter() ngOnInit(): void { } - updateUser(projectUsers : ProjectUser[]){ - this.projectUsers = projectUsers - } - - updateProjectUser(projectUser : ProjectUser){ - let index = this.projectUsers.findIndex(x => x.userID == projectUser.userID) - if (index > -1) { - this.projectUsers[index] = projectUser - } - this.updateProjectUsers.emit(this.projectUsers) - } - removeProjectUser(projectUser : ProjectUser){ let index = this.projectUsers.indexOf(projectUser,0) if (index > -1) { diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-user/project-user.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-user/project-user.component.html index 195908ab..1b4a01e6 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-user/project-user.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-user/project-user.component.html @@ -9,11 +9,9 @@
{{projectUser.user.userName}}
- +
- + {{projectUser.userRole | titlecase}}
@@ -22,4 +20,4 @@
- \ No newline at end of file + diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-user/project-user.component.ts b/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-user/project-user.component.ts index 1ead5d00..fd691ee5 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-user/project-user.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-list-users/project-user/project-user.component.ts @@ -12,26 +12,11 @@ export class ProjectUserComponent implements OnInit { constructor(private projectService : ProjectService) { } @Input() projectUser : ProjectUser - @Output() updatedUser = new EventEmitter() @Output() removedUser = new EventEmitter() - roles: any[] = ['member', 'admin']; - ngOnInit(): void { } - updateUser(event : Event){ - var role = (event.target as HTMLSelectElement).value; - this.projectUser.userRole = role.toLowerCase(); - this.projectService.updateUser(this.projectUser).subscribe( - result => { - this.updatedUser.emit(this.projectUser) - }, error =>{ - console.log(error) - } - ) - } - deleteUser(){ this.projectService.removeUser(this.projectUser.projectID, this.projectUser.userID).subscribe( result => { diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project/project.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project/project.component.html index 5a0e9dc0..c0d89cf2 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project/project.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project/project.component.html @@ -111,11 +111,77 @@

-
+