diff --git a/backend/src/modules/boards/controller/boards.controller.ts b/backend/src/modules/boards/controller/boards.controller.ts index c7ed07049..15e04df67 100644 --- a/backend/src/modules/boards/controller/boards.controller.ts +++ b/backend/src/modules/boards/controller/boards.controller.ts @@ -65,11 +65,12 @@ export default class BoardsController { @UseGuards(JwtAuthenticationGuard) @Get() - getAllBoards(@Req() request: RequestWithUser, @Query() { page, size }: PaginationParams) { + async getAllBoards(@Req() request: RequestWithUser, @Query() { page, size }: PaginationParams) { const { _id: userId, isSAdmin } = request.user; if (isSAdmin) { return this.getBoardApp.getSuperAdminBoards(userId, page, size); } + return this.getBoardApp.getUsersBoards(userId, page, size); } diff --git a/backend/src/modules/boards/interfaces/services/get.board.service.interface.ts b/backend/src/modules/boards/interfaces/services/get.board.service.interface.ts index 59b062996..d493308f2 100644 --- a/backend/src/modules/boards/interfaces/services/get.board.service.interface.ts +++ b/backend/src/modules/boards/interfaces/services/get.board.service.interface.ts @@ -1,6 +1,6 @@ -import { LeanDocument } from 'mongoose'; +import { Document, LeanDocument } from 'mongoose'; -import { BoardDocument } from '../../schemas/board.schema'; +import Board, { BoardDocument } from '../../schemas/board.schema'; import { BoardsAndPage } from '../boards-page.interface'; export interface GetBoardServiceInterface { @@ -29,5 +29,9 @@ export interface GetBoardServiceInterface { | null >; + getMainBoardData( + boardId: string + ): Promise & { _id: any }> | null>; + countBoards(userId: string): Promise; } diff --git a/backend/src/modules/boards/services/create.board.service.ts b/backend/src/modules/boards/services/create.board.service.ts index 86f702d37..029820186 100644 --- a/backend/src/modules/boards/services/create.board.service.ts +++ b/backend/src/modules/boards/services/create.board.service.ts @@ -63,11 +63,17 @@ export default class CreateBoardServiceImpl implements CreateBoardService { } async createBoard(boardData: BoardDto, userId: string, isSubBoard = false) { - const { dividedBoards = [] } = boardData; + const { dividedBoards = [], team } = boardData; + + /** + * Add in each divided board the team id (from main board) + */ + const dividedBoardsWithTeam = dividedBoards.map((dividedBoard) => ({ ...dividedBoard, team })); + return this.boardModel.create({ ...boardData, createdBy: userId, - dividedBoards: await this.createDividedBoards(dividedBoards, userId), + dividedBoards: await this.createDividedBoards(dividedBoardsWithTeam, userId), isSubBoard }); } diff --git a/backend/src/modules/boards/services/get.board.service.ts b/backend/src/modules/boards/services/get.board.service.ts index dcc44f055..5884bac8e 100644 --- a/backend/src/modules/boards/services/get.board.service.ts +++ b/backend/src/modules/boards/services/get.board.service.ts @@ -102,22 +102,36 @@ export default class GetBoardServiceImpl implements GetBoardServiceInterface { select: 'user role', populate: { path: 'user', - select: 'firstName lastName joinedAt' + select: '_id firstName lastName joinedAt' } } }) .populate({ path: 'dividedBoards', select: '-__v -createdAt -id', - populate: { - path: 'users', - select: 'role user', - populate: { - path: 'user', - model: 'User', - select: 'firstName lastName joinedAt' + populate: [ + { + path: 'users', + select: 'role user', + populate: { + path: 'user', + model: 'User', + select: 'firstName lastName joinedAt' + } + }, + { + path: 'team', + select: 'name users _id', + populate: { + path: 'users', + select: 'user role', + populate: { + path: 'user', + select: '_id firstName lastName joinedAt' + } + } } - } + ] }) .populate({ path: 'users', diff --git a/backend/src/modules/boards/services/update.board.service.ts b/backend/src/modules/boards/services/update.board.service.ts index 1bcbeea2d..8a6f87d7d 100644 --- a/backend/src/modules/boards/services/update.board.service.ts +++ b/backend/src/modules/boards/services/update.board.service.ts @@ -1,45 +1,115 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { LeanDocument, Model, ObjectId } from 'mongoose'; +import { TeamRoles } from 'libs/enum/team.roles'; import { GetTeamServiceInterface } from 'modules/teams/interfaces/services/get.team.service.interface'; import * as Teams from 'modules/teams/interfaces/types'; +import { TeamUserDocument } from 'modules/teams/schemas/team.user.schema'; import { UpdateBoardDto } from '../dto/update-board.dto'; import { UpdateBoardService } from '../interfaces/services/update.board.service.interface'; import Board, { BoardDocument } from '../schemas/board.schema'; +import BoardUser, { BoardUserDocument } from '../schemas/board.user.schema'; @Injectable() export default class UpdateBoardServiceImpl implements UpdateBoardService { constructor( @InjectModel(Board.name) private boardModel: Model, @Inject(Teams.TYPES.services.GetTeamService) - private getTeamService: GetTeamServiceInterface + private getTeamService: GetTeamServiceInterface, + @InjectModel(BoardUser.name) + private boardUserModel: Model ) {} + /** + * Method to retrieve user details from team. + * This method is used to see if the user is Admin or a Stakeholder + * + * @param userId Current User Logged + * @param teamId Team ID (team from board) + * @returns Team User + */ + private async getTeamUser( + userId: string, + teamId: string + ): Promise> { + const teamUser = await this.getTeamService.getTeamUser(userId, teamId); + + if (!teamUser) { + throw new NotFoundException('User not found on this team!'); + } + + return teamUser; + } + + /** + * Method to get user from board, if it's responsible + * If not, return a null value + * + * @param userId Current User Logged + * @returns Board User + */ + private async getResponsible(userId: string): Promise | null> { + const user = await this.boardUserModel.findOne({ user: userId }).lean().exec(); + + if (!user) { + throw new NotFoundException('User not found!'); + } + + if (user.role !== 'responsible') { + return null; + } + + return user; + } + async update(userId: string, boardId: string, boardData: UpdateBoardDto) { - const currentVotes = await this.boardModel.findById(boardId, 'maxVotes totalUsedVotes').exec(); + const board = await this.boardModel.findById(boardId).exec(); - return this.boardModel - .findOneAndUpdate( - { - _id: boardId, - createdBy: userId - }, - { - ...boardData, - maxVotes: - Number(boardData.maxVotes) < Number(currentVotes?.maxVotes) && - currentVotes?.totalUsedVotes !== 0 - ? currentVotes?.maxVotes - : boardData.maxVotes - }, - { - new: true - } - ) - .lean() - .exec(); + if (!board) { + throw new NotFoundException('Board not found!'); + } + + // Destructuring board variables + const { isSubBoard, team, createdBy } = board; + + // Get Team User to see if is Admin or Stakeholder + const teamUser = await this.getTeamUser(userId, String(team)); + + // Role Validation + const isAdminOrStakeholder = [TeamRoles.STAKEHOLDER, TeamRoles.ADMIN].includes( + teamUser.role as TeamRoles + ); + + // Get user info to see if is responsible or not + const subBoardResponsible = await this.getResponsible(userId); + + // Validate if the logged user are the owner + const isOwner = String(userId) === String(createdBy); + + if (isAdminOrStakeholder || isOwner || (isSubBoard && !!subBoardResponsible)) { + return this.boardModel + .findOneAndUpdate( + { + _id: boardId + }, + { + ...boardData, + maxVotes: + Number(boardData.maxVotes) < Number(board?.maxVotes) && board?.totalUsedVotes !== 0 + ? board?.maxVotes + : boardData.maxVotes + }, + { + new: true + } + ) + .lean() + .exec(); + } + + throw new ForbiddenException('You are not allowed to update this board!'); } async mergeBoards(subBoardId: string, userId: string) { diff --git a/frontend/src/components/Board/Settings/index.tsx b/frontend/src/components/Board/Settings/index.tsx index e630c62a2..3b42481d3 100644 --- a/frontend/src/components/Board/Settings/index.tsx +++ b/frontend/src/components/Board/Settings/index.tsx @@ -1,4 +1,4 @@ -import React, { Dispatch, ReactNode, SetStateAction, useEffect, useState } from 'react'; +import React, { Dispatch, ReactNode, SetStateAction, useEffect, useRef, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useRecoilState, useRecoilValue } from 'recoil'; import { joiResolver } from '@hookform/resolvers/joi'; @@ -38,6 +38,8 @@ type Props = { }; const BoardSettings = ({ isOpen, setIsOpen, socketId }: Props) => { + const submitBtnRef = useRef(null); + const [updateBoardData, setUpdateBoardData] = useRecoilState(updateBoardDataState); const haveError = useRecoilValue(updateBoardError); @@ -169,6 +171,29 @@ const BoardSettings = ({ isOpen, setIsOpen, socketId }: Props) => { ); }; + /** + * Use Effect to submit the board settings form when press enter key + * (Note: Radix Dialog close when pressing enter) + */ + useEffect(() => { + const keyDownHandler = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + + if (submitBtnRef.current) { + submitBtnRef.current.click(); + } + } + }; + + document.addEventListener('keydown', keyDownHandler); + + return () => { + document.removeEventListener('keydown', keyDownHandler); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const configurationSettings = ( title: string, text: string, @@ -309,6 +334,7 @@ const BoardSettings = ({ isOpen, setIsOpen, socketId }: Props) => { Cancel