diff --git a/.all-contributorsrc b/.all-contributorsrc index b029b66b5..ccc289fc2 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -119,6 +119,40 @@ "code", "doc" ] + }, + { + "login": "CatiaAntunes96", + "name": "CΓ‘tia Antunes", + "avatar_url": "https://avatars.githubusercontent.com/u/59372326?v=4", + "profile": "https://github.com/CatiaAntunes96", + "contributions": [ + "code", + "doc", + "review" + ] + }, + { + "login": "mourabraz", + "name": "mourabraz", + "avatar_url": "https://avatars.githubusercontent.com/u/7543719?v=4", + "profile": "https://www.linkedin.com/in/moura-braz", + "contributions": [ + "code", + "doc", + "review" + ] + }, + { + "login": "miguel-felix1", + "name": "Miguel FΓ©lix", + "avatar_url": "https://avatars.githubusercontent.com/u/87712174?v=4", + "profile": "https://github.com/miguel-felix1", + "contributions": [ + "code", + "bug", + "review", + "userTesting" + ] } ], "contributorsPerLine": 7, @@ -126,5 +160,6 @@ "projectOwner": "xgeekshq", "repoType": "github", "repoHost": "https://github.com", - "skipCi": false + "skipCi": false, + "commitConvention": "angular" } diff --git a/README.md b/README.md index a8a891925..6dc42b913 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ - [πŸ™ŒπŸ» How to Contribute](#--how-to-contribute) - [πŸƒ How to Run](https://github.com/xgeekshq/split/wiki/How-to-run) - [πŸ“ƒ Requirements](https://github.com/xgeekshq/split/wiki/Requirements) -- [Usage](#usage) +- [:computer: Usage](#usage) - [πŸ“ License](#-license) - [Contributors ✨](#contributors-) @@ -35,10 +35,9 @@ Check out our [**Contributing Guide**](.github/CONTRIBUTING.md) for information The backend will run on `http://localhost:BACKEND_PORT` and the frontend on `http://localhost:3000`. - `/dashboard`: dashboard +- `/boards`: boards list - `/boards/[boardId]`: board page -You must register to access the dashboard page. - ## πŸ“ License Licensed under the [MIT License](./LICENSE). @@ -51,21 +50,26 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + +

Nuno Caseiro

πŸ’» ⚠️ πŸš‡ πŸ“–

Gonçalo Dias

πŸ’» πŸ‘€ πŸ“– πŸ“†

Rui Silva

πŸ’» πŸ‘€

RΓΊben Carreira

πŸ’» πŸ‘€

Daniel Sousa

πŸ’» πŸ“– πŸ‘€

f-morgado

πŸ’» πŸ“–

r-dmatos

πŸ’» πŸ“–

CatiaBarroco-xgeeks

πŸ’» πŸ“–

JoΓ£o Santos

πŸ’» πŸ“–

LuΓ­s Francisco

πŸ’» πŸ“–

Geomar Bastiani

πŸ’» πŸ“–
Nuno Caseiro
Nuno Caseiro

πŸ’» ⚠️ πŸš‡ πŸ“–
Gonçalo Dias
Gonçalo Dias

πŸ’» πŸ‘€ πŸ“– πŸ“†
Rui Silva
Rui Silva

πŸ’» πŸ‘€
RΓΊben Carreira
RΓΊben Carreira

πŸ’» πŸ‘€
Daniel Sousa
Daniel Sousa

πŸ’» πŸ“– πŸ‘€
f-morgado
f-morgado

πŸ’» πŸ“–
r-dmatos
r-dmatos

πŸ’» πŸ“–
CatiaBarroco-xgeeks
CatiaBarroco-xgeeks

πŸ’» πŸ“–
JoΓ£o Santos
JoΓ£o Santos

πŸ’» πŸ“–
LuΓ­s Francisco
LuΓ­s Francisco

πŸ’» πŸ“–
Geomar Bastiani
Geomar Bastiani

πŸ’» πŸ“–
CΓ‘tia Antunes
CΓ‘tia Antunes

πŸ’» πŸ“– πŸ‘€
mourabraz
mourabraz

πŸ’» πŸ“– πŸ‘€
Miguel FΓ©lix
Miguel FΓ©lix

πŸ’» πŸ› πŸ‘€ πŸ““
diff --git a/backend/package-lock.json b/backend/package-lock.json index aea10b758..a0831e4d0 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13901,9 +13901,9 @@ } }, "node_modules/vm2": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.10.tgz", - "integrity": "sha512-AuECTSvwu2OHLAZYhG716YzwodKCIJxB6u1zG7PgSQwIgAlEaoXH52bxdcvT8GkGjnYK7r7yWDW0m0sOsPuBjQ==", + "version": "3.9.11", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.11.tgz", + "integrity": "sha512-PFG8iJRSjvvBdisowQ7iVF580DXb1uCIiGaXgm7tynMR1uTBlv7UJlB1zdv5KJ+Tmq1f0Upnj3fayoEOPpCBKg==", "dependencies": { "acorn": "^8.7.0", "acorn-walk": "^8.2.0" @@ -24929,9 +24929,9 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "vm2": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.10.tgz", - "integrity": "sha512-AuECTSvwu2OHLAZYhG716YzwodKCIJxB6u1zG7PgSQwIgAlEaoXH52bxdcvT8GkGjnYK7r7yWDW0m0sOsPuBjQ==", + "version": "3.9.11", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.11.tgz", + "integrity": "sha512-PFG8iJRSjvvBdisowQ7iVF580DXb1uCIiGaXgm7tynMR1uTBlv7UJlB1zdv5KJ+Tmq1f0Upnj3fayoEOPpCBKg==", "requires": { "acorn": "^8.7.0", "acorn-walk": "^8.2.0" diff --git a/backend/src/libs/dto/param/team.param.optional.ts b/backend/src/libs/dto/param/team.param.optional.ts new file mode 100644 index 000000000..7b8ce2e3f --- /dev/null +++ b/backend/src/libs/dto/param/team.param.optional.ts @@ -0,0 +1,6 @@ +import { IsOptional } from 'class-validator'; + +export class TeamParamOptional { + @IsOptional() + teamId?: string; +} diff --git a/backend/src/modules/boards/controller/boards.controller.ts b/backend/src/modules/boards/controller/boards.controller.ts index fd2eaf021..7108fd871 100644 --- a/backend/src/modules/boards/controller/boards.controller.ts +++ b/backend/src/modules/boards/controller/boards.controller.ts @@ -49,6 +49,7 @@ import { UnauthorizedResponse } from 'libs/swagger/errors/unauthorized.swagger'; import { BoardResponse } from 'modules/boards/swagger/board.swagger'; import SocketGateway from 'modules/socket/gateway/socket.gateway'; +import { TeamParamOptional } from '../../../libs/dto/param/team.param.optional'; import BoardDto from '../dto/board.dto'; import { UpdateBoardDto } from '../dto/update-board.dto'; import { CreateBoardApplicationInterface } from '../interfaces/applications/create.board.application.interface'; @@ -238,10 +239,20 @@ export default class BoardsController { type: InternalServerErrorResponse }) @Delete(':boardId') - async deleteBoard(@Param() { boardId }: BaseParam, @Req() request: RequestWithUser) { + async deleteBoard( + @Param() { boardId }: BaseParam, + @Query() { teamId }: TeamParamOptional, + @Query() { socketId }: BaseParamWSocket, + @Req() request: RequestWithUser + ) { const result = await this.deleteBoardApp.delete(boardId, request.user._id); if (!result) throw new BadRequestException(DELETE_FAILED); + if (socketId && teamId) { + this.socketService.sendUpdatedBoards(socketId, teamId); + this.socketService.sendUpdatedBoard(boardId, socketId); + } + return result; } diff --git a/backend/src/modules/queue/queue.module.ts b/backend/src/modules/queue/queue.module.ts index fe1d53b72..8fc9b8308 100644 --- a/backend/src/modules/queue/queue.module.ts +++ b/backend/src/modules/queue/queue.module.ts @@ -8,7 +8,6 @@ import { ConfigService } from '@nestjs/config'; inject: [ConfigService], useFactory: (configService: ConfigService) => ({ redis: { - username: configService.get('redis.user'), password: configService.get('redis.password'), host: configService.get('redis.host'), port: configService.get('redis.port'), diff --git a/backend/src/modules/schedules/services/create.schedules.service.ts b/backend/src/modules/schedules/services/create.schedules.service.ts index 61aa24d8b..f8ec0ffb0 100644 --- a/backend/src/modules/schedules/services/create.schedules.service.ts +++ b/backend/src/modules/schedules/services/create.schedules.service.ts @@ -44,7 +44,6 @@ export class CreateSchedulesService implements CreateSchedulesServiceInterface { job.start(); } catch (e) { await this.schedulesModel.deleteOne({ board: boardId }); - this.schedulerRegistry.deleteCronJob(boardId); } } diff --git a/backend/src/modules/socket/gateway/socket.gateway.ts b/backend/src/modules/socket/gateway/socket.gateway.ts index a01c0edfc..30ed7f07c 100644 --- a/backend/src/modules/socket/gateway/socket.gateway.ts +++ b/backend/src/modules/socket/gateway/socket.gateway.ts @@ -9,6 +9,7 @@ import { } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; +import JoinPayload from '../interfaces/joinPayload.interface'; import JoinPayloadBoards from '../interfaces/joinPayloadBoards.interface'; @WebSocketGateway({ cors: true }) @@ -24,8 +25,8 @@ export default class SocketGateway this.server.to(newBoardId.toString()).except(excludedClient).emit('updateAllBoard', newBoardId); } - sendUpdatedBoards(excludedClient: string) { - this.server.except(excludedClient).emit('updateBoardList'); + sendUpdatedBoards(excludedClient: string, teamId: string) { + this.server.to(teamId).except(excludedClient).emit('teamId'); } sendUpdatedAllBoard(excludedClient: string) { @@ -44,8 +45,13 @@ export default class SocketGateway this.logger.log(`Client connected: ${client.id}`); } + @SubscribeMessage('join') + handleJoin(client: Socket, payload: JoinPayload) { + client.join(payload.boardId); + } + @SubscribeMessage('joinBoards') handleJoinBoards(client: Socket, payload: JoinPayloadBoards) { - client.join(payload.boards); + client.join(payload.teamId); } } diff --git a/backend/src/modules/socket/interfaces/joinPayloadBoards.interface.ts b/backend/src/modules/socket/interfaces/joinPayloadBoards.interface.ts index 9364daad7..3a8f317d0 100644 --- a/backend/src/modules/socket/interfaces/joinPayloadBoards.interface.ts +++ b/backend/src/modules/socket/interfaces/joinPayloadBoards.interface.ts @@ -1,3 +1,3 @@ export default interface JoinPayloadBoards { - boards: string; + teamId: string; } diff --git a/frontend/src/api/boardService.tsx b/frontend/src/api/boardService.tsx index 0ede42184..360fac0b9 100644 --- a/frontend/src/api/boardService.tsx +++ b/frontend/src/api/boardService.tsx @@ -49,8 +49,16 @@ export const getBoardsRequest = ( return fetchData(`/boards?page=${pageParam ?? 0}&size=10`, { context, serverSide: !!context }); }; -export const deleteBoardRequest = async (id: string): Promise => { - return fetchData(`/boards/${id}`, { method: 'DELETE' }); +export const deleteBoardRequest = async ({ + id, + socketId, + teamId +}: { + id: string; + socketId?: string; + teamId: string; +}): Promise => { + return fetchData(`/boards/${id}`, { method: 'DELETE', params: { socketId, teamId } }); }; // #endregion diff --git a/frontend/src/components/Board/Header/index.tsx b/frontend/src/components/Board/Header/index.tsx index e2e918510..0fe71e5f3 100644 --- a/frontend/src/components/Board/Header/index.tsx +++ b/frontend/src/components/Board/Header/index.tsx @@ -149,6 +149,7 @@ const BoardHeader = () => { {isSubBoard ? title.replace('board', '') : team.name} ({ + const initialData: UpdateBoardType = { _id: board?._id, hideCards: board?.hideCards, hideVotes: board?.hideVotes, title: board?.title, maxVotes: board?.maxVotes, users: board?.users - }); + }; + const [data, setData] = useState(initialData); // References const dialogContainerRef = useRef(null); @@ -87,10 +88,8 @@ const BoardSettings = ({ updateBoard: { mutate } } = useBoard({ autoFetchBoard: false }); - const responsible = useMemo( - () => data.users?.find((user) => user.role === BoardUserRoles.RESPONSIBLE)?.user, - [data] - ); + const responsible = data.users?.find((user) => user.role === BoardUserRoles.RESPONSIBLE)?.user; + // Use Form Hook const methods = useForm<{ title: string; maxVotes?: number | null }>({ mode: 'onBlur', @@ -105,6 +104,7 @@ const BoardSettings = ({ // Method to close dialog and reset switches state const handleClose = () => { setIsOpen(false); + setData(initialData); setSwitchesState({ maxVotes: false, responsible: false diff --git a/frontend/src/components/Boards/MyBoards/index.tsx b/frontend/src/components/Boards/MyBoards/index.tsx index 7aa787b91..2220434df 100644 --- a/frontend/src/components/Boards/MyBoards/index.tsx +++ b/frontend/src/components/Boards/MyBoards/index.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { useInfiniteQuery } from 'react-query'; import { useSetRecoilState } from 'recoil'; @@ -7,6 +7,7 @@ import CardBody from 'components/CardBoard/CardBody/CardBody'; import LoadingPage from 'components/loadings/LoadingPage'; import Flex from 'components/Primitives/Flex'; import Text from 'components/Primitives/Text'; +import { useSocketBoardIO } from 'hooks/useSocketBoardIO'; import { toastState } from 'store/toast/atom/toast.atom'; import BoardType from 'types/board/board'; import { Team } from 'types/team/team'; @@ -46,6 +47,19 @@ const MyBoards = React.memo(({ userId, isSuperAdmin }) => { ); const { data, isLoading } = fetchBoards; + + const teamSocketId = data?.pages[0].boards[0] ? data?.pages[0].boards[0].team._id : undefined; + + // socketId + const { socket, queryClient } = useSocketBoardIO(teamSocketId); + + useEffect(() => { + if (!socket) return; + socket.on('teamId', () => { + queryClient.invalidateQueries('boards'); + }); + }, [socket, queryClient]); + const currentDate = new Date().toDateString(); const dataByTeamAndDate = useMemo(() => { @@ -84,10 +98,6 @@ const MyBoards = React.memo(({ userId, isSuperAdmin }) => { } }; - // const teamNames = Array.from(dataByTeamAndDate.teams.values()).map((team) => { - // return { value: team._id, label: team.name }; - // }); - return ( {/* */} @@ -176,6 +186,7 @@ const MyBoards = React.memo(({ userId, isSuperAdmin }) => { dividedBoardsCount={board.dividedBoards.length} isDashboard={false} isSAdmin={isSuperAdmin} + socketId={socket?.id} userId={userId} /> ))} diff --git a/frontend/src/components/Boards/MyBoards/styles.tsx b/frontend/src/components/Boards/MyBoards/styles.tsx index e760ad167..5cc736383 100644 --- a/frontend/src/components/Boards/MyBoards/styles.tsx +++ b/frontend/src/components/Boards/MyBoards/styles.tsx @@ -4,7 +4,7 @@ import Flex from 'components/Primitives/Flex'; const ScrollableContent = styled(Flex, { mt: '$24', - height: 'calc(100vh - 300px)', + height: 'calc(100vh - 180px)', overflowY: 'auto', pr: '$10', pb: '$10' diff --git a/frontend/src/components/CardBoard/CardAvatars.tsx b/frontend/src/components/CardBoard/CardAvatars.tsx index 2afef5ba4..27933255e 100644 --- a/frontend/src/components/CardBoard/CardAvatars.tsx +++ b/frontend/src/components/CardBoard/CardAvatars.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import Avatar from 'components/Primitives/Avatar'; import Flex from 'components/Primitives/Flex'; @@ -6,6 +6,7 @@ import Tooltip from 'components/Primitives/Tooltip'; import { User } from 'types/user/user'; import { BoardUserRoles } from 'utils/enums/board.user.roles'; import { TeamUserRoles } from 'utils/enums/team.user.roles'; +import { IconButton } from './styles'; type ListUsersType = { user: User | string; @@ -21,10 +22,26 @@ type CardAvatarProps = { userId: string; myBoards?: boolean; haveError?: boolean; + isBoardsPage?: boolean; }; const CardAvatars = React.memo( - ({ listUsers, teamAdmins, stakeholders, userId, haveError, responsible, myBoards }) => { + ({ + listUsers, + teamAdmins, + stakeholders, + userId, + haveError, + responsible, + myBoards, + isBoardsPage + }) => { + const [viewAllUsers, setViewAllUsers] = useState(false); + + const handleViewAllUsers = useCallback(() => { + setViewAllUsers(!viewAllUsers); + }, [viewAllUsers]); + const data = useMemo(() => { if (responsible) return listUsers @@ -43,7 +60,7 @@ const CardAvatars = React.memo( } return listUsers.reduce((acc: User[], userFound: ListUsersType) => { - if ((userFound.user as User)._id === userId) { + if ((userFound.user as User)?._id === userId) { acc.unshift(userFound.user as User); } else { acc.push(userFound.user as User); @@ -56,12 +73,12 @@ const CardAvatars = React.memo( const getInitials = useCallback( (user: User | undefined, index) => { - if (usersCount - 1 > index && index > 1) { + if (!viewAllUsers && usersCount - 1 > index && index > 1) { return `+${usersCount - 2}`; } return user ?? '--'; }, - [usersCount] + [usersCount, viewAllUsers] ); const stakeholdersColors = useMemo( @@ -77,15 +94,30 @@ const CardAvatars = React.memo( (value: User | string, avatarColor, idx) => { if (typeof value === 'string') { return ( - 0 ? '-7px' : 0 }} - fallbackText={value} - id={value} - isDefaultColor={value === userId} - size={32} - /> + aria-hidden="true" + disabled={!isBoardsPage} + type="button" + css={{ + '&:hover': isBoardsPage + ? { + cursor: 'pointer' + } + : 'none' + }} + onClick={handleViewAllUsers} + > + 0 ? '-7px' : 0 }} + fallbackText={value} + id={value} + isDefaultColor={value === userId} + size={32} + /> + ); } @@ -95,7 +127,19 @@ const CardAvatars = React.memo( key={`${value}-${idx}-${Math.random()}`} content={`${value.firstName} ${value.lastName}`} > -
+
+ ); }, - [userId] + [handleViewAllUsers, userId, isBoardsPage] ); + const numberOfAvatars = useMemo(() => { + if (!myBoards) { + return viewAllUsers && isBoardsPage ? data.length : 3; + } + + return 1; + }, [data.length, isBoardsPage, myBoards, viewAllUsers]); + return ( {haveError @@ -122,7 +174,7 @@ const CardAvatars = React.memo( index ); }) - : (data.slice(0, !myBoards ? 3 : 1) as User[]).map( + : (data.slice(0, numberOfAvatars) as User[]).map( (user: User, index: number) => { return renderAvatar( getInitials(user, index), diff --git a/frontend/src/components/CardBoard/CardBody/CardBody.tsx b/frontend/src/components/CardBoard/CardBody/CardBody.tsx index d6f74b1fa..67287e64d 100644 --- a/frontend/src/components/CardBoard/CardBody/CardBody.tsx +++ b/frontend/src/components/CardBoard/CardBody/CardBody.tsx @@ -90,20 +90,30 @@ type CardBodyProps = { isDashboard: boolean; mainBoardId?: string; isSAdmin?: boolean; + socketId?: string; }; const CardBody = React.memo( - ({ userId, board, index, isDashboard, dividedBoardsCount, mainBoardId, isSAdmin }) => { + ({ + userId, + board, + index, + isDashboard, + dividedBoardsCount, + mainBoardId, + isSAdmin, + socketId + }) => { const { _id: id, columns, users, team, dividedBoards, isSubBoard } = board; const countDividedBoards = dividedBoardsCount || dividedBoards.length; - const [openSubBoards, setSubBoardsOpen] = useState(true); + const [openSubBoards, setSubBoardsOpen] = useState(false); const newBoard = useRecoilValue(newBoardState); const isANewBoard = newBoard?._id === board._id; const userIsParticipating = useMemo(() => { - return !!users.find((user) => user.user._id === userId); + return !!users.find((user) => user.user?._id === userId); }, [users, userId]); const havePermissions = useMemo(() => { @@ -136,11 +146,12 @@ const CardBody = React.memo( index={idx} isDashboard={isDashboard} mainBoardId={board._id} + socketId={socketId} userId={userId} /> ); }, - [board._id, countDividedBoards, isDashboard, userId] + [board._id, countDividedBoards, isDashboard, userId, socketId] ); const iconLockConditions = @@ -237,6 +248,7 @@ const CardBody = React.memo( index={index} isDashboard={isDashboard} isSubBoard={isSubBoard} + socketId={socketId} userId={userId} userIsParticipating={userIsParticipating} userSAdmin={isSAdmin} diff --git a/frontend/src/components/CardBoard/CardBody/CardEnd.tsx b/frontend/src/components/CardBoard/CardBody/CardEnd.tsx index f22a827ff..bcb4581bb 100644 --- a/frontend/src/components/CardBoard/CardBody/CardEnd.tsx +++ b/frontend/src/components/CardBoard/CardBody/CardEnd.tsx @@ -17,6 +17,7 @@ type CardEndProps = { userId: string; userSAdmin?: boolean; userIsParticipating: boolean; + socketId?: string; }; const CardEnd: React.FC = React.memo( @@ -27,7 +28,8 @@ const CardEnd: React.FC = React.memo( index, havePermissions, userId, - userSAdmin = undefined + userSAdmin = undefined, + socketId }) => { CardEnd.defaultProps = { userSAdmin: undefined @@ -102,7 +104,12 @@ const CardEnd: React.FC = React.memo( }} /> - + )} diff --git a/frontend/src/components/CardBoard/DeleteBoard.tsx b/frontend/src/components/CardBoard/DeleteBoard.tsx index 67b7511d2..a54f3af60 100644 --- a/frontend/src/components/CardBoard/DeleteBoard.tsx +++ b/frontend/src/components/CardBoard/DeleteBoard.tsx @@ -5,13 +5,18 @@ import Flex from 'components/Primitives/Flex'; import Tooltip from 'components/Primitives/Tooltip'; import useBoard from 'hooks/useBoard'; -type DeleteBoardProps = { boardId: string; boardName: string }; +type DeleteBoardProps = { + boardId: string; + boardName: string; + socketId?: string; + teamId: string; +}; -const DeleteBoard: React.FC = ({ boardId, boardName }) => { +const DeleteBoard: React.FC = ({ boardId, boardName, socketId, teamId }) => { const { deleteBoard } = useBoard({ autoFetchBoard: false }); const handleDelete = () => { - deleteBoard.mutate(boardId); + deleteBoard.mutate({ id: boardId, socketId, teamId }); }; return ( diff --git a/frontend/src/components/CardBoard/styles.tsx b/frontend/src/components/CardBoard/styles.tsx new file mode 100644 index 000000000..8288a7dd3 --- /dev/null +++ b/frontend/src/components/CardBoard/styles.tsx @@ -0,0 +1,8 @@ +import { styled } from 'styles/stitches/stitches.config'; + +const IconButton = styled('button', { + border: 'none', + backgroundColor: 'transparent' +}); + +export { IconButton }; diff --git a/frontend/src/components/CreateBoard/SubTeamsTab/TeamSubTeamsConfigurations.tsx b/frontend/src/components/CreateBoard/SubTeamsTab/TeamSubTeamsConfigurations.tsx index d872b3f14..58c769a1d 100644 --- a/frontend/src/components/CreateBoard/SubTeamsTab/TeamSubTeamsConfigurations.tsx +++ b/frontend/src/components/CreateBoard/SubTeamsTab/TeamSubTeamsConfigurations.tsx @@ -36,7 +36,10 @@ const TeamSubTeamsConfigurations: React.FC = ({ const [stakeholders, setStakeholders] = useState([]); const [team, setTeam] = useState(null); - const { data: teams } = useQuery(['teams'], () => getAllTeams(), { suspense: false }); + const { data: teams } = useQuery(['teams'], () => getAllTeams(), { + suspense: false, + refetchOnWindowFocus: false + }); const setBoardData = useSetRecoilState(createBoardDataState); const [haveError, setHaveError] = useRecoilState(createBoardError); @@ -128,16 +131,14 @@ const TeamSubTeamsConfigurations: React.FC = ({ Stakeholders - {!haveError && stakeholders && stakeholders.length === 1 - ? stakeholders.map( - ({ firstName, lastName }) => `${firstName} ${lastName}` - ) - : ''} - {!haveError && stakeholders && stakeholders.length !== 1 - ? stakeholders.map( - ({ firstName, lastName }) => `${firstName} ${lastName}, ` - ) - : ''} + {!haveError && + stakeholders && + stakeholders.length > 0 && + stakeholders.map((value, index) => { + return index < stakeholders.length - 1 + ? `${value.firstName} ${value.lastName}, ` + : `${value.firstName} ${value.lastName}`; + })} diff --git a/frontend/src/components/Dashboard/RecentRetros/index.tsx b/frontend/src/components/Dashboard/RecentRetros/index.tsx index d191899da..dec33fac6 100644 --- a/frontend/src/components/Dashboard/RecentRetros/index.tsx +++ b/frontend/src/components/Dashboard/RecentRetros/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { useInfiniteQuery } from 'react-query'; import { useSetRecoilState } from 'recoil'; @@ -6,6 +6,7 @@ import { getDashboardBoardsRequest } from 'api/boardService'; import { toastState } from 'store/toast/atom/toast.atom'; import { ToastStateEnum } from 'utils/enums/toast-types'; import isEmpty from 'utils/isEmpty'; +import { useSocketBoardIO } from '../../../hooks/useSocketBoardIO'; import EmptyBoards from './partials/EmptyBoards'; import ListOfCards from './partials/ListOfCards'; @@ -38,6 +39,17 @@ const RecentRetros = React.memo(({ userId }) => { ); const { data, isFetching } = fetchDashboardBoards; + + const teamSocketId = data?.pages[0].boards[0] ? data?.pages[0].boards[0].team._id : undefined; + + // socketId + const { socket, queryClient } = useSocketBoardIO(teamSocketId); + useEffect(() => { + if (!socket) return; + socket.on('teamId', () => { + queryClient.invalidateQueries('boards/dashboard'); + }); + }, [socket, queryClient]); if (!data || isEmpty(data?.pages[0].boards)) return ; return ( (({ data, userId, fetchBoards, is return ( = ({ strategy }) => { Users - - + + Teams - + diff --git a/frontend/src/hooks/useBoard.tsx b/frontend/src/hooks/useBoard.tsx index ca8537963..0e2e06bef 100644 --- a/frontend/src/hooks/useBoard.tsx +++ b/frontend/src/hooks/useBoard.tsx @@ -18,7 +18,7 @@ interface AutoFetchProps { autoFetchBoard: boolean; } -const useBoard = ({ autoFetchBoard }: AutoFetchProps): UseBoardType => { +const useBoard = ({ autoFetchBoard = false }: AutoFetchProps): UseBoardType => { const { boardId, queryClient, setToastState } = useBoardUtils(); const setBoard = useSetRecoilState(boardState); diff --git a/frontend/src/hooks/useCreateBoard.tsx b/frontend/src/hooks/useCreateBoard.tsx index 70f5535c7..84192b4aa 100644 --- a/frontend/src/hooks/useCreateBoard.tsx +++ b/frontend/src/hooks/useCreateBoard.tsx @@ -1,13 +1,13 @@ import { useCallback, useMemo } from 'react'; import { useRecoilState, useResetRecoilState } from 'recoil'; +import { TeamUserRoles } from 'utils/enums/team.user.roles'; import { createBoardDataState } from '../store/createBoard/atoms/create-board.atom'; import { BoardToAdd } from '../types/board/board'; import { BoardUserToAdd } from '../types/board/board.user'; import { Team } from '../types/team/team'; import { TeamUser } from '../types/team/team.user'; import { BoardUserRoles } from '../utils/enums/board.user.roles'; -import { TeamUserRoles } from '../utils/enums/team.user.roles'; const useCreateBoard = (team: Team) => { const [createBoardData, setCreateBoardData] = useRecoilState(createBoardDataState); @@ -19,10 +19,9 @@ const useCreateBoard = (team: Team) => { const MIN_MEMBERS = 4; const teamMembers = team.users.filter( - (teamUser) => - teamUser.role !== TeamUserRoles.STAKEHOLDER && - new Date(teamUser.user.joinedAt).getTime() > 0 + (teamUser) => teamUser.role !== TeamUserRoles.STAKEHOLDER ); + const dividedBoardsCount = board.dividedBoards.length; const generateSubBoard = useCallback( diff --git a/frontend/src/hooks/useSocketBoardIO.ts b/frontend/src/hooks/useSocketBoardIO.ts new file mode 100644 index 000000000..52be76ea9 --- /dev/null +++ b/frontend/src/hooks/useSocketBoardIO.ts @@ -0,0 +1,34 @@ +import { useEffect, useState } from 'react'; +import { QueryClient, useQueryClient } from 'react-query'; +import { io, Socket } from 'socket.io-client'; + +import { NEXT_PUBLIC_BACKEND_URL } from 'utils/constants'; + +type UseSocketBoardInterface = { + socket: Socket | null; + queryClient: QueryClient; +}; + +export const useSocketBoardIO = (teamId: string | undefined): UseSocketBoardInterface => { + const queryClient = useQueryClient(); + const [socket, setSocket] = useState(null); + + useEffect(() => { + const newSocket: Socket = io(NEXT_PUBLIC_BACKEND_URL ?? 'http://127.0.0.1:3200', { + transports: ['polling'] + }); + + newSocket.on('connect', () => { + newSocket.emit('joinBoards', { teamId }); + setSocket(newSocket); + }); + + return () => { + newSocket.disconnect(); + setSocket(null); + }; + }, [teamId, queryClient, setSocket]); + + // return socket?.id; + return { socket, queryClient }; +}; diff --git a/frontend/src/pages/board-deleted.tsx b/frontend/src/pages/board-deleted.tsx new file mode 100644 index 000000000..351b74b34 --- /dev/null +++ b/frontend/src/pages/board-deleted.tsx @@ -0,0 +1,47 @@ +import Link from 'next/link'; + +import { + BannerContainer, + ContainerSection, + GoBackButton, + ImageBackground +} from 'styles/pages/error.styles'; + +import Banner from 'components/icons/Banner'; +import LogoIcon from 'components/icons/Logo'; +import Text from 'components/Primitives/Text'; + +export default function Custom404() { + return ( + + + + + + + + + + 404 + + + + Board deleted + + + The board was deleted by a board admin + + + + Go to Dashboard + + + + + ); +} diff --git a/frontend/src/pages/boards/[boardId].tsx b/frontend/src/pages/boards/[boardId].tsx index cff72afd4..1a2222adb 100644 --- a/frontend/src/pages/boards/[boardId].tsx +++ b/frontend/src/pages/boards/[boardId].tsx @@ -1,6 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { dehydrate, QueryClient } from 'react-query'; import { GetServerSideProps, NextPage } from 'next'; +import { useRouter } from 'next/router'; import { useSession } from 'next-auth/react'; import { useRecoilState, useSetRecoilState } from 'recoil'; @@ -25,9 +26,18 @@ import isEmpty from 'utils/isEmpty'; export const getServerSideProps: GetServerSideProps = async (context) => { const { boardId } = context.query; const queryClient = new QueryClient(); - await queryClient.prefetchQuery(['board', { id: boardId }], () => - getBoardRequest(boardId as string, context) - ); + try { + await queryClient.fetchQuery(['board', { id: boardId }], () => + getBoardRequest(boardId as string, context) + ); + } catch (e) { + return { + redirect: { + permanent: false, + destination: '/dashboard' + } + }; + } return { props: { key: context.query.boardId, @@ -64,6 +74,7 @@ const Board: NextPage = ({ boardId, mainBoardId }) => { }); const mainBoard = data?.mainBoardData; const board = data?.board; + const route = useRouter(); // Socket IO Hook const socketId = useSocketIO(boardId); @@ -124,8 +135,13 @@ const Board: NextPage = ({ boardId, mainBoardId }) => { const userIsInBoard = board?.users.find((user) => user.user._id === userId); - if (!userIsInBoard && !hasAdminRole) return ; + useEffect(() => { + if (data === null) { + route.push('/board-deleted'); + } + }, [data, route]); + if (!userIsInBoard && !hasAdminRole) return ; return board && userId && socketId ? ( <> diff --git a/frontend/src/types/board/useBoard.ts b/frontend/src/types/board/useBoard.ts index 607d35608..b943ec440 100644 --- a/frontend/src/types/board/useBoard.ts +++ b/frontend/src/types/board/useBoard.ts @@ -10,6 +10,11 @@ export default interface UseBoardType { UpdateBoardType & { socketId: string }, unknown >; - deleteBoard: UseMutationResult; + deleteBoard: UseMutationResult< + BoardType, + unknown, + { id: string; socketId?: string; teamId: string }, + unknown + >; fetchBoard: UseQueryResult; } diff --git a/frontend/src/utils/fetchData.tsx b/frontend/src/utils/fetchData.tsx index d976da0d7..18dc41e6a 100644 --- a/frontend/src/utils/fetchData.tsx +++ b/frontend/src/utils/fetchData.tsx @@ -52,12 +52,15 @@ const fetchData = async (url: string, options?: Options): Promise => { Authorization: refreshToken ? `Bearer ${refreshToken}` : await getToken(context) }; } + try { + const { data } = !serverSide + ? await instance(instanceOptions) + : await serverSideInstance(instanceOptions); - const { data } = !serverSide - ? await instance(instanceOptions) - : await serverSideInstance(instanceOptions); - - return data; + return data; + } catch { + return null as any; + } }; export default fetchData;