From 98a174756c4453a9c0ec4d170699194758e9f850 Mon Sep 17 00:00:00 2001 From: Nickolas Gupton Date: Sun, 10 Nov 2024 00:33:32 -0600 Subject: [PATCH] Create project and project dashboard --- .../Controllers/ProjectController.cs | 77 ++++- Backend/Bones.Api/OpenApi/swagger.json | 191 ++++++++++- Backend/Bones.Backend/Bones.Backend.csproj | 1 - .../CreateProject/CreateProjectHandler.cs | 2 +- .../GetProjectById/GetProjectByIdHandler.cs | 30 ++ .../GetProjectById/GetProjectByIdQuery.cs | 11 + .../GetProjectByIdQueryValidator.cs | 13 + .../GetProjectsByOwnerHandler.cs | 21 +- .../GetProjectsByOwnerQuery.cs | 2 +- .../GetProjectsUserCanAccessHandler.cs | 25 ++ .../GetProjectsUserCanAccessQuery.cs | 9 + .../GetProjectsUserCanAccessQueryValidator.cs | 14 + .../UserHasProjectPermissionHandler.cs | 2 +- .../GetProjectsByOwnerDbHandler.cs | 18 + .../GetProjectsByOwnerDbQuery.cs | 11 + .../GetProjectsByOwnerDbQueryValidator.cs | 12 + Bones.Shared/Consts/BonesClaimTypes.cs | 318 +++++++++--------- Bones.Shared/Consts/FrontEndUrls.cs | 18 + .../Bones.Api.Client/AutoGenBonesApiClient.cs | 259 +++++++++++++- Frontend/Bones.WebUI/Layout/MainLayout.razor | 28 +- .../Bones.WebUI/Layout/MainLayout.razor.cs | 75 +++-- .../Pages/Account/ConfirmEmailPage.razor.cs | 2 +- .../Pages/Account/LoginPage.razor.cs | 2 +- .../Pages/Account/MyProfilePage.razor.cs | 3 +- .../Pages/Account/RegisterPage.razor.cs | 3 +- .../Pages/Project/CreateProjectPage.razor | 30 ++ .../Pages/Project/CreateProjectPage.razor.cs | 52 +++ .../Pages/Project/ProjectDashboardPage.razor | 13 + .../Project/ProjectDashboardPage.razor.cs | 51 +++ 29 files changed, 1076 insertions(+), 217 deletions(-) create mode 100644 Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectById/GetProjectByIdHandler.cs create mode 100644 Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectById/GetProjectByIdQuery.cs create mode 100644 Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectById/GetProjectByIdQueryValidator.cs create mode 100644 Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessHandler.cs create mode 100644 Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQuery.cs create mode 100644 Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQueryValidator.cs create mode 100644 Backend/Bones.Database/Operations/ProjectManagement/Projects/GetProjectsByOwnerDb/GetProjectsByOwnerDbHandler.cs create mode 100644 Backend/Bones.Database/Operations/ProjectManagement/Projects/GetProjectsByOwnerDb/GetProjectsByOwnerDbQuery.cs create mode 100644 Backend/Bones.Database/Operations/ProjectManagement/Projects/GetProjectsByOwnerDb/GetProjectsByOwnerDbQueryValidator.cs create mode 100644 Frontend/Bones.WebUI/Pages/Project/CreateProjectPage.razor create mode 100644 Frontend/Bones.WebUI/Pages/Project/CreateProjectPage.razor.cs create mode 100644 Frontend/Bones.WebUI/Pages/Project/ProjectDashboardPage.razor create mode 100644 Frontend/Bones.WebUI/Pages/Project/ProjectDashboardPage.razor.cs diff --git a/Backend/Bones.Api/Controllers/ProjectController.cs b/Backend/Bones.Api/Controllers/ProjectController.cs index 03eabd2..0074e15 100644 --- a/Backend/Bones.Api/Controllers/ProjectController.cs +++ b/Backend/Bones.Api/Controllers/ProjectController.cs @@ -1,7 +1,11 @@ +using System.Text.Json.Serialization; using Bones.Api.Models; using Bones.Backend.Features.ProjectManagement.Projects.CreateProject; +using Bones.Backend.Features.ProjectManagement.Projects.GetProjectById; using Bones.Backend.Features.ProjectManagement.Projects.GetProjectsByOwner; +using Bones.Backend.Features.ProjectManagement.Projects.GetProjectsUserCanAccess; using Bones.Database.DbSets.AccountManagement; +using Bones.Database.DbSets.ProjectManagement; using Bones.Shared.Backend.Enums; using Bones.Shared.Backend.Models; using Microsoft.AspNetCore.Mvc; @@ -51,14 +55,14 @@ public record GetProjectsByOwnerRequest(OwnershipType OwnerType, Guid? Organizat /// Gets the projects for the current user, or specified organization /// /// The request - /// Created if created, otherwise BadRequest with a message of what went wrong. - [HttpGet("projects", Name = "GetProjectsByOwnerAsync")] - [ProducesResponseType>(StatusCodes.Status200OK)] + /// Ok with the results if successful, otherwise BadRequest with a message of what went wrong. + [HttpGet("projects/by-owner", Name = "GetProjectsByOwnerAsync")] + [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType>(StatusCodes.Status400BadRequest)] public async Task GetProjectsByOwnerAsync([FromBody] GetProjectsByOwnerRequest request) { BonesUser currentUser = await GetCurrentBonesUserAsync(); - QueryResponse> response = await Sender.Send(new GetProjectsByOwnerQuery(request.OwnerType, request.OrganizationId ?? currentUser.Id, currentUser)); + QueryResponse> response = await Sender.Send(new GetProjectsByOwnerQuery(request.OwnerType, request.OrganizationId ?? currentUser.Id, currentUser)); if (!response.Success) { return BadRequest(response.FailureReasons); @@ -66,4 +70,69 @@ public async Task GetProjectsByOwnerAsync([FromBody] GetProjectsBy return Ok(response.Result); } + + /// + /// Gets the projects the current user is able to access + /// + /// Ok with the results if successful, otherwise BadRequest with a message of what went wrong. + [HttpGet("projects", Name = "GetProjectsUserCanAccessAsync")] + [ProducesResponseType>(StatusCodes.Status200OK)] + [ProducesResponseType>(StatusCodes.Status400BadRequest)] + public async Task GetProjectsUserCanAccessAsync() + { + BonesUser currentUser = await GetCurrentBonesUserAsync(); + QueryResponse> response = await Sender.Send(new GetProjectsUserCanAccessQuery(currentUser)); + if (!response.Success) + { + return BadRequest(response.FailureReasons); + } + + return Ok(response.Result); + } + + /// + /// + /// + /// + /// + /// + /// + /// + [Serializable] + [JsonSerializable(typeof(GetProjectDashboardResponse))] + public record GetProjectDashboardResponse( + Guid ProjectId, + string ProjectName, + int InitiativeCount, + OwnershipType OwnerType, + Guid OwnerId + ); + + /// + /// Gets a projects dashboard information + /// + /// + /// Ok with the results if successful, otherwise BadRequest with a message of what went wrong. + [HttpGet("{projectId:guid}/dashboard", Name = "GetProjectDashboardAsync")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType>(StatusCodes.Status400BadRequest)] + public async Task GetProjectDashboardAsync(Guid projectId) + { + BonesUser currentUser = await GetCurrentBonesUserAsync(); + QueryResponse response = await Sender.Send(new GetProjectByIdQuery(projectId, currentUser)); + if (!response.Success || response.Result == null) + { + return BadRequest(response.FailureReasons); + } + + // We know they won't be null + Guid ownerId = response.Result.OwnerType == OwnershipType.User + ? response.Result.OwningUser!.Id + : response.Result.OwningOrganization!.Id; + + GetProjectDashboardResponse resp = new(response.Result.Id, response.Result.Name, + response.Result.Initiatives.Count, response.Result.OwnerType, ownerId); + + return Ok(resp); + } } \ No newline at end of file diff --git a/Backend/Bones.Api/OpenApi/swagger.json b/Backend/Bones.Api/OpenApi/swagger.json index b4bf057..fae6a3c 100644 --- a/Backend/Bones.Api/OpenApi/swagger.json +++ b/Backend/Bones.Api/OpenApi/swagger.json @@ -586,7 +586,7 @@ } } }, - "/Project/projects": { + "/Project/projects/by-owner": { "get": { "tags": [ "Project" @@ -639,9 +639,79 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GuidStringValueTuple" + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/Project/projects": { + "get": { + "tags": [ + "Project" + ], + "summary": "Gets the projects the current user is able to access", + "operationId": "GetProjectsUserCanAccessAsync", + "responses": { + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmptyResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmptyResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmptyResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" } } } @@ -666,6 +736,85 @@ } } }, + "/Project/{projectId}/dashboard": { + "get": { + "tags": [ + "Project" + ], + "summary": "Gets a projects dashboard information", + "operationId": "GetProjectDashboardAsync", + "parameters": [ + { + "name": "projectId", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmptyResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmptyResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmptyResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetProjectDashboardResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, "/Test/bones-exception": { "get": { "tags": [ @@ -859,6 +1008,36 @@ "additionalProperties": false, "description": "" }, + "GetProjectDashboardResponse": { + "type": "object", + "properties": { + "projectId": { + "type": "string", + "description": "", + "format": "uuid" + }, + "projectName": { + "type": "string", + "description": "", + "nullable": true + }, + "initiativeCount": { + "type": "integer", + "description": "", + "format": "int32" + }, + "ownerType": { + "$ref": "#/components/schemas/OwnershipType" + }, + "ownerId": { + "type": "string", + "description": "", + "format": "uuid" + } + }, + "additionalProperties": false, + "description": "" + }, "GetProjectsByOwnerRequest": { "type": "object", "properties": { @@ -875,10 +1054,6 @@ "additionalProperties": false, "description": "Request to get the projects for a given User/Organization" }, - "GuidStringValueTuple": { - "type": "object", - "additionalProperties": false - }, "LoginUserApiRequest": { "type": "object", "properties": { diff --git a/Backend/Bones.Backend/Bones.Backend.csproj b/Backend/Bones.Backend/Bones.Backend.csproj index 7ee6843..f67528f 100644 --- a/Backend/Bones.Backend/Bones.Backend.csproj +++ b/Backend/Bones.Backend/Bones.Backend.csproj @@ -26,7 +26,6 @@ - diff --git a/Backend/Bones.Backend/Features/ProjectManagement/Projects/CreateProject/CreateProjectHandler.cs b/Backend/Bones.Backend/Features/ProjectManagement/Projects/CreateProject/CreateProjectHandler.cs index 43f7050..e5148d5 100644 --- a/Backend/Bones.Backend/Features/ProjectManagement/Projects/CreateProject/CreateProjectHandler.cs +++ b/Backend/Bones.Backend/Features/ProjectManagement/Projects/CreateProject/CreateProjectHandler.cs @@ -22,7 +22,7 @@ public async Task Handle(CreateProjectCommand request, Cancella return CommandResponse.Forbid(); } - const string perm = BonesClaimTypes.Role.Organization.Project.CREATE_PROJECT; + const string perm = BonesClaimTypes.Role.Project.CREATE_PROJECT; bool? hasOrganizationPermission = await sender.Send(new UserHasOrganizationPermissionQuery(organization.Id, request.RequestingUser, perm), cancellationToken); diff --git a/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectById/GetProjectByIdHandler.cs b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectById/GetProjectByIdHandler.cs new file mode 100644 index 0000000..b5715d2 --- /dev/null +++ b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectById/GetProjectByIdHandler.cs @@ -0,0 +1,30 @@ +using Bones.Backend.Features.ProjectManagement.Projects.UserHasProjectPermission; +using Bones.Database.DbSets.ProjectManagement; +using Bones.Database.Operations.ProjectManagement.Projects.GetProjectByIdDb; +using Bones.Shared.Consts; + +namespace Bones.Backend.Features.ProjectManagement.Projects.GetProjectById; + +internal sealed class GetProjectByIdHandler(ISender sender) : IRequestHandler> +{ + public async Task> Handle(GetProjectByIdQuery request, CancellationToken cancellationToken) + { + const string perm = BonesClaimTypes.Role.Project.VIEW_PROJECT; + bool? hasPermission = + await sender.Send(new UserHasProjectPermissionQuery(request.ProjectId, request.RequestingUser, perm), cancellationToken); + + if (hasPermission is not true) + { + return QueryResponse.Forbid(); + } + + Project? project = await sender.Send(new GetProjectByIdDbQuery(request.ProjectId), cancellationToken); + + if (project is null) + { + return QueryResponse.Fail("Project not found"); + } + + return QueryResponse.Pass(project); + } +} \ No newline at end of file diff --git a/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectById/GetProjectByIdQuery.cs b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectById/GetProjectByIdQuery.cs new file mode 100644 index 0000000..88c95f1 --- /dev/null +++ b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectById/GetProjectByIdQuery.cs @@ -0,0 +1,11 @@ +using Bones.Database.DbSets.AccountManagement; +using Bones.Database.DbSets.ProjectManagement; + +namespace Bones.Backend.Features.ProjectManagement.Projects.GetProjectById; + +/// +/// +/// +/// +/// +public sealed record GetProjectByIdQuery(Guid ProjectId, BonesUser RequestingUser) : IRequest>; \ No newline at end of file diff --git a/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectById/GetProjectByIdQueryValidator.cs b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectById/GetProjectByIdQueryValidator.cs new file mode 100644 index 0000000..c992d6d --- /dev/null +++ b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectById/GetProjectByIdQueryValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation.Results; + +namespace Bones.Backend.Features.ProjectManagement.Projects.GetProjectById; + +internal sealed class GetProjectByIdQueryValidator : AbstractValidator +{ + public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new()) + { + RuleFor(x => x.RequestingUser).NotNull(); + + return base.ValidateAsync(context, cancellation); + } +} \ No newline at end of file diff --git a/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsByOwner/GetProjectsByOwnerHandler.cs b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsByOwner/GetProjectsByOwnerHandler.cs index 9439f4a..7aa6453 100644 --- a/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsByOwner/GetProjectsByOwnerHandler.cs +++ b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsByOwner/GetProjectsByOwnerHandler.cs @@ -1,22 +1,31 @@ +using Bones.Database.DbSets.ProjectManagement; +using Bones.Database.Operations.ProjectManagement.Projects.GetProjectsByOwnerDb; using Bones.Shared.Backend.Enums; namespace Bones.Backend.Features.ProjectManagement.Projects.GetProjectsByOwner; -internal sealed class GetProjectsByOwnerHandler(ISender sender) : IRequestHandler>> +internal sealed class GetProjectsByOwnerHandler(ISender sender) : IRequestHandler>> { - public async Task>> Handle(GetProjectsByOwnerQuery request, CancellationToken cancellationToken) + public async Task>> Handle(GetProjectsByOwnerQuery request, CancellationToken cancellationToken) { if (request.OwnerType == OwnershipType.User && request.OwnerId == request.RequestingUser.Id) { - throw new NotImplementedException(); + List? projects = await sender.Send(new GetProjectsByOwnerDbQuery(OwnershipType.User, request.RequestingUser.Id), cancellationToken); + + if (projects is null) + { + return QueryResponse>.Fail("DB failed :("); + } + + return projects.ToDictionary(project => project.Id, project => project.Name); } else if (request.OwnerType == OwnershipType.Organization) { - // TODO - await sender.Send(new(), cancellationToken); + // TODO: Not implemented, will fail. Need to also check perms here before requesting DB + await sender.Send(new GetProjectsByOwnerDbQuery(OwnershipType.Organization, request.OwnerId), cancellationToken); } - return QueryResponse>.Forbid(); + return QueryResponse>.Forbid(); } } \ No newline at end of file diff --git a/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsByOwner/GetProjectsByOwnerQuery.cs b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsByOwner/GetProjectsByOwnerQuery.cs index edd1892..c4a3899 100644 --- a/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsByOwner/GetProjectsByOwnerQuery.cs +++ b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsByOwner/GetProjectsByOwnerQuery.cs @@ -9,4 +9,4 @@ namespace Bones.Backend.Features.ProjectManagement.Projects.GetProjectsByOwner; /// /// /// -public sealed record GetProjectsByOwnerQuery(OwnershipType OwnerType, Guid OwnerId, BonesUser RequestingUser) : IRequest>>; \ No newline at end of file +public sealed record GetProjectsByOwnerQuery(OwnershipType OwnerType, Guid OwnerId, BonesUser RequestingUser) : IRequest>>; \ No newline at end of file diff --git a/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessHandler.cs b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessHandler.cs new file mode 100644 index 0000000..2cc2b04 --- /dev/null +++ b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessHandler.cs @@ -0,0 +1,25 @@ +using Bones.Backend.Features.ProjectManagement.Projects.GetProjectsByOwner; +using Bones.Database.DbSets.ProjectManagement; +using Bones.Database.Operations.ProjectManagement.Projects.GetProjectsByOwnerDb; +using Bones.Shared.Backend.Enums; + +namespace Bones.Backend.Features.ProjectManagement.Projects.GetProjectsUserCanAccess; + +internal sealed class GetProjectsUserCanAccessHandler(ISender sender) : IRequestHandler>> +{ + public async Task>> Handle(GetProjectsUserCanAccessQuery request, CancellationToken cancellationToken) + { + List? projects = await sender.Send(new GetProjectsByOwnerDbQuery(OwnershipType.User, request.RequestingUser.Id), cancellationToken); + + if (projects is null) + { + return QueryResponse>.Fail("DB failed :("); + } + + Dictionary projectsUserCanAccess = projects.ToDictionary(project => project.Id, project => project.Name); + + // TODO: add projects from organizations + + return projectsUserCanAccess; + } +} \ No newline at end of file diff --git a/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQuery.cs b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQuery.cs new file mode 100644 index 0000000..ff2efb8 --- /dev/null +++ b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQuery.cs @@ -0,0 +1,9 @@ +using Bones.Database.DbSets.AccountManagement; + +namespace Bones.Backend.Features.ProjectManagement.Projects.GetProjectsUserCanAccess; + +/// +/// +/// +/// +public sealed record GetProjectsUserCanAccessQuery(BonesUser RequestingUser) : IRequest>>; \ No newline at end of file diff --git a/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQueryValidator.cs b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQueryValidator.cs new file mode 100644 index 0000000..c2966dc --- /dev/null +++ b/Backend/Bones.Backend/Features/ProjectManagement/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQueryValidator.cs @@ -0,0 +1,14 @@ +using Bones.Backend.Features.ProjectManagement.Projects.GetProjectsByOwner; +using FluentValidation.Results; + +namespace Bones.Backend.Features.ProjectManagement.Projects.GetProjectsUserCanAccess; + +internal sealed class GetProjectsUserCanAccessQueryValidator : AbstractValidator +{ + public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new()) + { + RuleFor(x => x.RequestingUser).NotNull(); + + return base.ValidateAsync(context, cancellation); + } +} \ No newline at end of file diff --git a/Backend/Bones.Backend/Features/ProjectManagement/Projects/UserHasProjectPermission/UserHasProjectPermissionHandler.cs b/Backend/Bones.Backend/Features/ProjectManagement/Projects/UserHasProjectPermission/UserHasProjectPermissionHandler.cs index cc1b04e..f2c9979 100644 --- a/Backend/Bones.Backend/Features/ProjectManagement/Projects/UserHasProjectPermission/UserHasProjectPermissionHandler.cs +++ b/Backend/Bones.Backend/Features/ProjectManagement/Projects/UserHasProjectPermission/UserHasProjectPermissionHandler.cs @@ -42,7 +42,7 @@ public async Task> Handle(UserHasProjectPermissionQuery requ return QueryResponse.Fail("Role not found"); } - string neededClaim = BonesClaimTypes.Role.Organization.Project.GetProjectClaimType(project.Id, request.Claim); + string neededClaim = BonesClaimTypes.Role.Project.GetProjectClaimType(project.Id, request.Claim); IList claims = await roleManager.GetClaimsAsync(role); if (claims.Any(claim => claim.Type == neededClaim && claim.Value == ClaimValues.YES)) diff --git a/Backend/Bones.Database/Operations/ProjectManagement/Projects/GetProjectsByOwnerDb/GetProjectsByOwnerDbHandler.cs b/Backend/Bones.Database/Operations/ProjectManagement/Projects/GetProjectsByOwnerDb/GetProjectsByOwnerDbHandler.cs new file mode 100644 index 0000000..b90d28e --- /dev/null +++ b/Backend/Bones.Database/Operations/ProjectManagement/Projects/GetProjectsByOwnerDb/GetProjectsByOwnerDbHandler.cs @@ -0,0 +1,18 @@ +using Bones.Database.DbSets.ProjectManagement; +using Bones.Shared.Backend.Enums; + +namespace Bones.Database.Operations.ProjectManagement.Projects.GetProjectsByOwnerDb; + +internal sealed class GetProjectsByOwnerDbHandler(BonesDbContext dbContext) : IRequestHandler>> +{ + public async Task>> Handle(GetProjectsByOwnerDbQuery request, CancellationToken cancellationToken) + { + if (request.OwnerType == OwnershipType.User) + { + List projects = await dbContext.Projects.Where(p => p.OwningUser != null && p.OwningUser.Id == request.OwnerId).ToListAsync(cancellationToken); + return QueryResponse>.Pass(projects); + } + + return QueryResponse>.Fail("org not implemented"); + } +} \ No newline at end of file diff --git a/Backend/Bones.Database/Operations/ProjectManagement/Projects/GetProjectsByOwnerDb/GetProjectsByOwnerDbQuery.cs b/Backend/Bones.Database/Operations/ProjectManagement/Projects/GetProjectsByOwnerDb/GetProjectsByOwnerDbQuery.cs new file mode 100644 index 0000000..73bdc2d --- /dev/null +++ b/Backend/Bones.Database/Operations/ProjectManagement/Projects/GetProjectsByOwnerDb/GetProjectsByOwnerDbQuery.cs @@ -0,0 +1,11 @@ +using Bones.Database.DbSets.ProjectManagement; +using Bones.Shared.Backend.Enums; + +namespace Bones.Database.Operations.ProjectManagement.Projects.GetProjectsByOwnerDb; + +/// +/// DB Query to get projects by its owner +/// +/// The owner type +/// The owner id +public sealed record GetProjectsByOwnerDbQuery(OwnershipType OwnerType, Guid OwnerId) : IRequest>>; diff --git a/Backend/Bones.Database/Operations/ProjectManagement/Projects/GetProjectsByOwnerDb/GetProjectsByOwnerDbQueryValidator.cs b/Backend/Bones.Database/Operations/ProjectManagement/Projects/GetProjectsByOwnerDb/GetProjectsByOwnerDbQueryValidator.cs new file mode 100644 index 0000000..22e5f5d --- /dev/null +++ b/Backend/Bones.Database/Operations/ProjectManagement/Projects/GetProjectsByOwnerDb/GetProjectsByOwnerDbQueryValidator.cs @@ -0,0 +1,12 @@ +namespace Bones.Database.Operations.ProjectManagement.Projects.GetProjectsByOwnerDb; + +internal sealed class GetProjectsByOwnerDbQueryValidator : AbstractValidator +{ + public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new()) + { + RuleFor(x => x.OwnerType).NotNull().IsInEnum(); + RuleFor(x => x.OwnerId).NotNull().NotEqual(Guid.Empty); + + return base.ValidateAsync(context, cancellation); + } +} \ No newline at end of file diff --git a/Bones.Shared/Consts/BonesClaimTypes.cs b/Bones.Shared/Consts/BonesClaimTypes.cs index 9478049..27da152 100644 --- a/Bones.Shared/Consts/BonesClaimTypes.cs +++ b/Bones.Shared/Consts/BonesClaimTypes.cs @@ -58,184 +58,186 @@ public static string GetOrganizationWideClaimType(Guid organizationId, string pe return $"O{organizationId}|{permissionName}"; } + } + + /// + /// Project level claim types for roles + /// + public static class Project + { /// - /// Project level claim types for organization roles + /// Claim type for if a user can view a project /// - public static class Project - { - /// - /// Claim type for if a user can view a project - /// - public const string VIEW_PROJECT = "ViewProject"; - - /// - /// Claim type for if a user can create a project - /// - public const string CREATE_PROJECT = "CreateProject"; - - /// - /// Claim type for if a user can delete a project - /// - public const string DELETE_PROJECT = "DeleteProject"; - - /// - /// Claim type for if a user can view the project settings - /// - public const string VIEW_PROJECT_SETTINGS = "ViewProjectSettings"; - - /// - /// Claim type for if a user can edit the project settings - /// - public const string EDIT_PROJECT_SETTINGS = "EditProjectSettings"; - - /// - /// Gets the claim type for permissions that should apply to this project - /// - /// - /// - /// - public static string GetProjectClaimType(Guid projectId, string permissionName) - { - return $"P{projectId}|{permissionName}"; - } - } + public const string VIEW_PROJECT = "ViewProject"; /// - /// Initiative level claim types for organization roles + /// Claim type for if a user can create a project /// - public static class Initiative - { - /// - /// Claim type for if a user can view an initiative - /// - public const string VIEW_INITIATIVE = "ViewInitiative"; - - /// - /// Claim type for if a user can create an initiative - /// - public const string CREATE_INITIATIVE = "CreateInitive"; - - /// - /// Claim type for if a user can delete an initiative - /// - public const string DELETE_INITIATIVE = "DeleteInitive"; - - /// - /// Claim type for if a user can view the initiatives settings - /// - public const string VIEW_INITIATIVE_SETTINGS = "ViewInitiativeSettings"; - - /// - /// Claim type for if a user can edit the initiatives settings - /// - public const string EDIT_INITIATIVE_SETTINGS = "EditInitiativeSettings"; - - - /// - /// Gets the claim type for permissions that should apply to this initiative - /// - /// - /// - /// - public static string GetInitiativeClaimType(Guid initiativeId, string permissionName) - { - return $"I{initiativeId}|{permissionName}"; - } - } + public const string CREATE_PROJECT = "CreateProject"; + + /// + /// Claim type for if a user can delete a project + /// + public const string DELETE_PROJECT = "DeleteProject"; /// - /// Queue level claim types for organization roles + /// Claim type for if a user can view the project settings /// - public static class Queue + public const string VIEW_PROJECT_SETTINGS = "ViewProjectSettings"; + + /// + /// Claim type for if a user can edit the project settings + /// + public const string EDIT_PROJECT_SETTINGS = "EditProjectSettings"; + + /// + /// Gets the claim type for permissions that should apply to this project + /// + /// + /// + /// + public static string GetProjectClaimType(Guid projectId, string permissionName) { - /// - /// Claim type for if a user can view a queue - /// - public const string VIEW_QUEUE = "ViewQueue"; - - /// - /// Claim type for if a user can create a queue - /// - public const string CREATE_QUEUE = "CreateQueue"; - - /// - /// Claim type for if a user can delete a queue - /// - public const string DELETE_QUEUE = "DeleteQueue"; - - /// - /// Claim type for if a user can view the queues settings - /// - public const string VIEW_QUEUE_SETTINGS = "ViewQueueSettings"; - - /// - /// Claim type for if a user can edit the queues settings - /// - public const string EDIT_QUEUE_SETTINGS = "EditQueueSettings"; - - /// - /// Gets the claim type for permissions that should apply to this queue - /// - /// - /// - /// - public static string GetQueueClaimType(Guid queueId, string permissionName) - { - return $"Q{queueId}|{permissionName}"; - } + return $"P{projectId}|{permissionName}"; } + } + + /// + /// Initiative level claim types for organization roles + /// + public static class Initiative + { + /// + /// Claim type for if a user can view an initiative + /// + public const string VIEW_INITIATIVE = "ViewInitiative"; + + /// + /// Claim type for if a user can create an initiative + /// + public const string CREATE_INITIATIVE = "CreateInitive"; + + /// + /// Claim type for if a user can delete an initiative + /// + public const string DELETE_INITIATIVE = "DeleteInitive"; + + /// + /// Claim type for if a user can view the initiatives settings + /// + public const string VIEW_INITIATIVE_SETTINGS = "ViewInitiativeSettings"; /// - /// Work Item level claim types for organization roles + /// Claim type for if a user can edit the initiatives settings /// - public static class WorkItem + public const string EDIT_INITIATIVE_SETTINGS = "EditInitiativeSettings"; + + + /// + /// Gets the claim type for permissions that should apply to this initiative + /// + /// + /// + /// + public static string GetInitiativeClaimType(Guid initiativeId, string permissionName) { - /// - /// Claim type for if a user can view a work item - /// - public const string VIEW_WORK_ITEM = "ViewWorkItem"; - - /// - /// Claim type for if a user can create a work item - /// - public const string CREATE_WORK_ITEM = "CreateWorkItem"; - - /// - /// Claim type for if a user can delete a work item - /// - public const string DELETE_WORK_ITEM = "DeleteWorkItem"; - - /// - /// Claim type for if a user can edit a work item - /// - public const string EDIT_WORK_ITEM = "EditWorkItem"; + return $"I{initiativeId}|{permissionName}"; } + } + + /// + /// Queue level claim types for organization roles + /// + public static class Queue + { + /// + /// Claim type for if a user can view a queue + /// + public const string VIEW_QUEUE = "ViewQueue"; + + /// + /// Claim type for if a user can create a queue + /// + public const string CREATE_QUEUE = "CreateQueue"; + + /// + /// Claim type for if a user can delete a queue + /// + public const string DELETE_QUEUE = "DeleteQueue"; + + /// + /// Claim type for if a user can view the queues settings + /// + public const string VIEW_QUEUE_SETTINGS = "ViewQueueSettings"; + + /// + /// Claim type for if a user can edit the queues settings + /// + public const string EDIT_QUEUE_SETTINGS = "EditQueueSettings"; /// - /// Asset level claim types for organization roles + /// Gets the claim type for permissions that should apply to this queue /// - public static class Asset + /// + /// + /// + public static string GetQueueClaimType(Guid queueId, string permissionName) { - /// - /// Claim type for if a user can view an asset - /// - public const string VIEW_ASSET = "ViewAsset"; - - /// - /// Claim type for if a user can create an asset - /// - public const string CREATE_ASSET = "CreateAsset"; - - /// - /// Claim type for if a user can delete an asset - /// - public const string DELETE_ASSET = "DeleteAsset"; - - /// - /// Claim type for if a user can edit an asset - /// - public const string EDIT_ASSET = "EditAsset"; + return $"Q{queueId}|{permissionName}"; } } + + /// + /// Work Item level claim types for organization roles + /// + public static class WorkItem + { + /// + /// Claim type for if a user can view a work item + /// + public const string VIEW_WORK_ITEM = "ViewWorkItem"; + + /// + /// Claim type for if a user can create a work item + /// + public const string CREATE_WORK_ITEM = "CreateWorkItem"; + + /// + /// Claim type for if a user can delete a work item + /// + public const string DELETE_WORK_ITEM = "DeleteWorkItem"; + + /// + /// Claim type for if a user can edit a work item + /// + public const string EDIT_WORK_ITEM = "EditWorkItem"; + } + + /// + /// Asset level claim types for organization roles + /// + public static class Asset + { + /// + /// Claim type for if a user can view an asset + /// + public const string VIEW_ASSET = "ViewAsset"; + + /// + /// Claim type for if a user can create an asset + /// + public const string CREATE_ASSET = "CreateAsset"; + + /// + /// Claim type for if a user can delete an asset + /// + public const string DELETE_ASSET = "DeleteAsset"; + + /// + /// Claim type for if a user can edit an asset + /// + public const string EDIT_ASSET = "EditAsset"; + } + } } \ No newline at end of file diff --git a/Bones.Shared/Consts/FrontEndUrls.cs b/Bones.Shared/Consts/FrontEndUrls.cs index 85ec084..e036e49 100644 --- a/Bones.Shared/Consts/FrontEndUrls.cs +++ b/Bones.Shared/Consts/FrontEndUrls.cs @@ -63,6 +63,24 @@ public static class Account public const string RESET_PASSWORD = $"{_account}/ResetPassword"; } + /// + /// Project pages + /// + public static class Project + { + private const string _project = "/Project"; + + /// + /// Create project page + /// + public const string CREATE = $"{_project}/Create"; + + /// + /// Project dashboard page + /// + public const string DASHBOARD = _project + "/{ProjectId:guid}/Dashboard/"; + } + /// /// System Admin pages /// diff --git a/Frontend/Bones.Api.Client/AutoGenBonesApiClient.cs b/Frontend/Bones.Api.Client/AutoGenBonesApiClient.cs index a11ea6a..266b979 100644 --- a/Frontend/Bones.Api.Client/AutoGenBonesApiClient.cs +++ b/Frontend/Bones.Api.Client/AutoGenBonesApiClient.cs @@ -1055,7 +1055,7 @@ private static System.Text.Json.JsonSerializerOptions CreateSerializerSettings() /// The request /// OK /// A server side error occurred. - public virtual async System.Threading.Tasks.Task> GetProjectsByOwnerAsync(GetProjectsByOwnerRequest body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + public virtual async System.Threading.Tasks.Task> GetProjectsByOwnerAsync(GetProjectsByOwnerRequest body = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var client_ = _httpClient; var disposeClient_ = false; @@ -1072,6 +1072,120 @@ private static System.Text.Json.JsonSerializerOptions CreateSerializerSettings() var urlBuilder_ = new System.Text.StringBuilder(); + // Operation Path: "Project/projects/by-owner" + urlBuilder_.Append("Project/projects/by-owner"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 401) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("Unauthorized", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 403) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("Forbidden", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 500) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("Internal Server Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync>(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 400) + { + var objectResponse_ = await ReadObjectResponseAsync>>(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException>>("Bad Request", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Gets the projects the current user is able to access + /// + /// OK + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task> GetProjectsUserCanAccessAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + // Operation Path: "Project/projects" urlBuilder_.Append("Project/projects"); @@ -1130,7 +1244,126 @@ private static System.Text.Json.JsonSerializerOptions CreateSerializerSettings() else if (status_ == 200) { - var objectResponse_ = await ReadObjectResponseAsync>(response_, headers_, cancellationToken).ConfigureAwait(false); + var objectResponse_ = await ReadObjectResponseAsync>(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 400) + { + var objectResponse_ = await ReadObjectResponseAsync>>(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException>>("Bad Request", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Gets a projects dashboard information + /// + /// OK + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task GetProjectDashboardAsync(System.Guid projectId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (projectId == null) + throw new System.ArgumentNullException("projectId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "Project/{projectId}/dashboard" + urlBuilder_.Append("Project/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(projectId, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/dashboard"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 401) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("Unauthorized", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 403) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("Forbidden", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 500) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("Internal Server Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); if (objectResponse_.Object == null) { throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); @@ -1551,6 +1784,28 @@ public partial record GetMyProfileResponse } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial record GetProjectDashboardResponse + { + + [System.Text.Json.Serialization.JsonPropertyName("projectId")] + public System.Guid? ProjectId { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("projectName")] + public string ProjectName { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("initiativeCount")] + public int? InitiativeCount { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("ownerType")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] + public OwnershipType? OwnerType { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("ownerId")] + public System.Guid? OwnerId { get; set; } + + } + /// /// Request to get the projects for a given User/Organization /// diff --git a/Frontend/Bones.WebUI/Layout/MainLayout.razor b/Frontend/Bones.WebUI/Layout/MainLayout.razor index 8e577cb..78f4c0e 100644 --- a/Frontend/Bones.WebUI/Layout/MainLayout.razor +++ b/Frontend/Bones.WebUI/Layout/MainLayout.razor @@ -1,6 +1,8 @@ @inherits LayoutComponentBase +@inject ILogger Logger - + + @@ -11,13 +13,23 @@ - - Go to project... - @foreach (ProjectDropDownModel proj in Projects) - { - @proj.ToString() - } - +
+ + + + @foreach (ProjectDropDownModel proj in Projects) + { + @proj.ToString() + } + +
diff --git a/Frontend/Bones.WebUI/Layout/MainLayout.razor.cs b/Frontend/Bones.WebUI/Layout/MainLayout.razor.cs index 722caf7..cdd2fc2 100644 --- a/Frontend/Bones.WebUI/Layout/MainLayout.razor.cs +++ b/Frontend/Bones.WebUI/Layout/MainLayout.razor.cs @@ -1,53 +1,84 @@ +using Bones.Shared.Consts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + namespace Bones.WebUI.Layout; /// /// /// -public partial class MainLayout +public partial class MainLayout : LayoutComponentBase { + private MudTheme? _theme = null; + + /// + /// The theme this app is going to use + /// + protected MudTheme Theme + { + get + { + if (_theme == null) + { + // TODO: Customize theme here + _theme = new(); + } + + return _theme; + } + } + private bool _open = false; private record ProjectDropDownModel { - public required string OrganizationName { get; init; } public required string ProjectName { get; init; } public required Guid? ProjectId { get; init; } public override string ToString() { - if (ProjectId == null || ProjectId == Guid.Empty) - { - return OrganizationName; - } - - return $"{OrganizationName} - {ProjectName}"; + return ProjectName; } } - private List Projects { get; set; } = []; + private List Projects { get; set; } = [new() + { + ProjectName = "(loading)", + ProjectId = null + }]; /// /// /// - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { - Projects.Add(new() + try { - OrganizationName = "+ Create a new project", - ProjectName = string.Empty, - ProjectId = null - }); + IDictionary projects = await ApiClient.GetProjectsUserCanAccessAsync(); - // TODO: Get users list of projects from the API and add them + Projects = []; + foreach (KeyValuePair proj in projects) + { + Projects.Add(new() + { + ProjectId = Guid.Parse(proj.Key), + ProjectName = proj.Value + }); + } + } + catch (Exception ex) + { + Logger.LogError(ex, ex.Message); + } Projects.Add(new() { - OrganizationName = "+ Create a new project", - ProjectName = string.Empty, + ProjectName = "+ Create a new project", ProjectId = Guid.Empty }); - base.OnInitialized(); + + await base.OnInitializedAsync(); } private void ToggleDrawer() @@ -62,13 +93,11 @@ private void OnGoToProjectChanged(IEnumerable? selectedProject) { if (selected.Value == Guid.Empty) { - // TODO: - // NavManager.NavigateTo(FrontEndUrls.Projects.CREATE); + NavManager.NavigateTo(FrontEndUrls.Project.CREATE); } else { - // TODO: - // NavManager.NavigateTo(FrontEndUrls.Projects.DASHBOARD.Replace("{{ProjectId}}", selected.Value.ToString()); + NavManager.NavigateTo(FrontEndUrls.Project.DASHBOARD.Replace("{ProjectId:guid}", selected.Value.ToString())); } } } diff --git a/Frontend/Bones.WebUI/Pages/Account/ConfirmEmailPage.razor.cs b/Frontend/Bones.WebUI/Pages/Account/ConfirmEmailPage.razor.cs index f2a9202..2aba6a2 100644 --- a/Frontend/Bones.WebUI/Pages/Account/ConfirmEmailPage.razor.cs +++ b/Frontend/Bones.WebUI/Pages/Account/ConfirmEmailPage.razor.cs @@ -5,7 +5,7 @@ namespace Bones.WebUI.Pages.Account; /// /// Confirms the users email /// -public partial class ConfirmEmailPage +public partial class ConfirmEmailPage : ComponentBase { /// /// The ID of the user this request is for diff --git a/Frontend/Bones.WebUI/Pages/Account/LoginPage.razor.cs b/Frontend/Bones.WebUI/Pages/Account/LoginPage.razor.cs index f62522f..c716289 100644 --- a/Frontend/Bones.WebUI/Pages/Account/LoginPage.razor.cs +++ b/Frontend/Bones.WebUI/Pages/Account/LoginPage.razor.cs @@ -7,7 +7,7 @@ namespace Bones.WebUI.Pages.Account; /// /// Page to login /// -public partial class LoginPage +public partial class LoginPage : ComponentBase { /// /// The URL to send them to after login is successful diff --git a/Frontend/Bones.WebUI/Pages/Account/MyProfilePage.razor.cs b/Frontend/Bones.WebUI/Pages/Account/MyProfilePage.razor.cs index 2dae76f..2455d73 100644 --- a/Frontend/Bones.WebUI/Pages/Account/MyProfilePage.razor.cs +++ b/Frontend/Bones.WebUI/Pages/Account/MyProfilePage.razor.cs @@ -1,6 +1,7 @@ using System.Globalization; using Bones.Api.Client; using Bones.Shared.Consts; +using Microsoft.AspNetCore.Components; using MudBlazor; namespace Bones.WebUI.Pages.Account; @@ -8,7 +9,7 @@ namespace Bones.WebUI.Pages.Account; /// /// The user can view and update their profile here /// -public partial class MyProfilePage +public partial class MyProfilePage : ComponentBase { private bool ProfileUpdateSuccess { get; set; } = false; diff --git a/Frontend/Bones.WebUI/Pages/Account/RegisterPage.razor.cs b/Frontend/Bones.WebUI/Pages/Account/RegisterPage.razor.cs index a276026..1c4ba92 100644 --- a/Frontend/Bones.WebUI/Pages/Account/RegisterPage.razor.cs +++ b/Frontend/Bones.WebUI/Pages/Account/RegisterPage.razor.cs @@ -1,4 +1,5 @@ using Bones.Shared; +using Microsoft.AspNetCore.Components; using MudBlazor; namespace Bones.WebUI.Pages.Account; @@ -6,7 +7,7 @@ namespace Bones.WebUI.Pages.Account; /// /// The page for registering user accounts /// -public partial class RegisterPage +public partial class RegisterPage : ComponentBase { /// /// Was the registration finished successfully? diff --git a/Frontend/Bones.WebUI/Pages/Project/CreateProjectPage.razor b/Frontend/Bones.WebUI/Pages/Project/CreateProjectPage.razor new file mode 100644 index 0000000..472d1fb --- /dev/null +++ b/Frontend/Bones.WebUI/Pages/Project/CreateProjectPage.razor @@ -0,0 +1,30 @@ +@attribute [Route(FrontEndUrls.Project.CREATE)] +@inject ILogger Logger +@layout AuthenticatedLayout + +

Create Project

+ + + + + + +
+ Create +
+
+
+
+ + + + @foreach (string error in ValidationErrors) + { + + @error + + } + +
\ No newline at end of file diff --git a/Frontend/Bones.WebUI/Pages/Project/CreateProjectPage.razor.cs b/Frontend/Bones.WebUI/Pages/Project/CreateProjectPage.razor.cs new file mode 100644 index 0000000..19919a6 --- /dev/null +++ b/Frontend/Bones.WebUI/Pages/Project/CreateProjectPage.razor.cs @@ -0,0 +1,52 @@ +using Bones.Shared; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Bones.WebUI.Pages.Project; + +/// +/// Create project page +/// +public partial class CreateProjectPage : ComponentBase +{ + /// + /// Did the request to the API result in an error? + /// + public bool ApiError { get; set; } = false; + + /// + /// Is the form valid? + /// + public bool FormValid { get; set; } + + /// + /// The issues with the users inputs + /// + public string[] ValidationErrors { get; set; } = []; + + private MudTextField ProjectName { get; set; } = new(); + + /// + /// Send the request to register to the API, if it errors tell the user what went wrong. + /// + public async Task SendCreateRequestAsync() + { + try + { + ApiError = false; + + await ApiClient.CreateProjectAsync(new() + { + Name = ProjectName.Text, + OrganizationId = null + }); + + // TODO: Forward to newly created projects dashboard + } + catch (Exception ex) + { + Logger.LogError(ex, "Error while registering user"); + ApiError = true; + } + } +} \ No newline at end of file diff --git a/Frontend/Bones.WebUI/Pages/Project/ProjectDashboardPage.razor b/Frontend/Bones.WebUI/Pages/Project/ProjectDashboardPage.razor new file mode 100644 index 0000000..d2ba62c --- /dev/null +++ b/Frontend/Bones.WebUI/Pages/Project/ProjectDashboardPage.razor @@ -0,0 +1,13 @@ +@attribute [Route(FrontEndUrls.Project.DASHBOARD)] +@inject ILogger Logger +@layout AuthenticatedLayout + +

Project Dashboard

+ + + Project ID: @ProjectId + Project Name: @ProjectName + Number of initiatives: @InitiativeCount + Owner Type: @OwnerType + Owner ID: @OwnerId + \ No newline at end of file diff --git a/Frontend/Bones.WebUI/Pages/Project/ProjectDashboardPage.razor.cs b/Frontend/Bones.WebUI/Pages/Project/ProjectDashboardPage.razor.cs new file mode 100644 index 0000000..5a0acc9 --- /dev/null +++ b/Frontend/Bones.WebUI/Pages/Project/ProjectDashboardPage.razor.cs @@ -0,0 +1,51 @@ +using Bones.Api.Client; +using Microsoft.AspNetCore.Components; + +namespace Bones.WebUI.Pages.Project; + +/// +/// Dashboard for projects +/// +public partial class ProjectDashboardPage : ComponentBase +{ + /// + /// The ID of the project to load in this dashboard + /// + [Parameter] + public Guid ProjectId { get; set; } + + /// + /// The name of the project, received from the API + /// + public string? ProjectName { get; set; } + + /// + /// The number of initiatives on the project, received from the API + /// + public int? InitiativeCount { get; set; } + + /// + /// The type of owner for the project, received from the API + /// + public OwnershipType? OwnerType { get; set; } + + /// + /// The ID of the owner of the project, received from the API + /// + public Guid? OwnerId { get; set; } + + /// + /// + /// + protected override async Task OnInitializedAsync() + { + GetProjectDashboardResponse dashboardResponse = await ApiClient.GetProjectDashboardAsync(ProjectId); + ProjectName = dashboardResponse.ProjectName; + InitiativeCount = dashboardResponse.InitiativeCount; + OwnerType = dashboardResponse.OwnerType; + OwnerId = dashboardResponse.OwnerId; + // @page "/Project/{ProjectId:guid}/Dashboard/" + // + await base.OnInitializedAsync(); + } +} \ No newline at end of file