diff --git a/backend/.gitignore b/backend/.gitignore index 39874a4ad..b63ad834b 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -44,4 +44,7 @@ lerna-debug.log* .env.dev .env.staging -.vscode \ No newline at end of file +.vscode + +# Log file for slack +slackLog.txt \ No newline at end of file diff --git a/backend/src/modules/boards/boards.module.ts b/backend/src/modules/boards/boards.module.ts index 4cc512b66..4533ab01d 100644 --- a/backend/src/modules/boards/boards.module.ts +++ b/backend/src/modules/boards/boards.module.ts @@ -4,6 +4,7 @@ import { mongooseBoardModule, mongooseBoardUserModule } from 'infrastructure/database/mongoose.module'; +import { CommunicationModule } from 'modules/communication/communication.module'; import { SchedulesModule } from 'modules/schedules/schedules.module'; import TeamsModule from 'modules/teams/teams.module'; import UsersModule from 'modules/users/users.module'; @@ -25,6 +26,7 @@ import BoardsController from './controller/boards.controller'; UsersModule, TeamsModule, SchedulesModule, + CommunicationModule, mongooseBoardModule, mongooseBoardUserModule ], diff --git a/backend/src/modules/boards/dto/board.dto.ts b/backend/src/modules/boards/dto/board.dto.ts index 250a021d5..8318ea5d1 100644 --- a/backend/src/modules/boards/dto/board.dto.ts +++ b/backend/src/modules/boards/dto/board.dto.ts @@ -100,4 +100,9 @@ export default class BoardDto { @IsNotEmpty() @IsBoolean() isSubBoard?: boolean; + + @ApiPropertyOptional({ default: false }) + @IsOptional() + @IsBoolean() + slackEnable?: boolean; } diff --git a/backend/src/modules/boards/services/create.board.service.ts b/backend/src/modules/boards/services/create.board.service.ts index 76b02bdad..7cd96c5ad 100644 --- a/backend/src/modules/boards/services/create.board.service.ts +++ b/backend/src/modules/boards/services/create.board.service.ts @@ -1,18 +1,26 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { LeanDocument, Model } from 'mongoose'; import { BoardRoles } from 'libs/enum/board.roles'; import { TeamRoles } from 'libs/enum/team.roles'; +import { + fillDividedBoardsUsersWithTeamUsers, + translateBoard +} from 'libs/utils/communication-helpers'; import { getDay, getNextMonth } from 'libs/utils/dates'; import { generateBoardDtoData, generateSubBoardDtoData } from 'libs/utils/generateBoardData'; import isEmpty from 'libs/utils/isEmpty'; +import { GetBoardServiceInterface } from 'modules/boards/interfaces/services/get.board.service.interface'; +import { TYPES } from 'modules/boards/interfaces/types'; +import { ExecuteCommunicationInterface } from 'modules/communication/interfaces/execute-communication.interface'; +import * as CommunicationsType from 'modules/communication/interfaces/types'; import { AddCronJobDto } from 'modules/schedules/dto/add.cronjob.dto'; import { CreateSchedulesServiceInterface } from 'modules/schedules/interfaces/services/create.schedules.service'; import * as SchedulesType from 'modules/schedules/interfaces/types'; import TeamDto from 'modules/teams/dto/team.dto'; import { GetTeamServiceInterface } from 'modules/teams/interfaces/services/get.team.service.interface'; -import { TYPES } from 'modules/teams/interfaces/types'; +import { TYPES as TeamType } from 'modules/teams/interfaces/types'; import TeamUser, { TeamUserDocument } from 'modules/teams/schemas/team.user.schema'; import { UserDocument } from 'modules/users/schemas/user.schema'; @@ -31,14 +39,20 @@ export interface CreateBoardDto { @Injectable() export default class CreateBoardServiceImpl implements CreateBoardService { + private logger = new Logger(CreateBoardServiceImpl.name); + constructor( @InjectModel(Board.name) private boardModel: Model, @InjectModel(BoardUser.name) private boardUserModel: Model, - @Inject(TYPES.services.GetTeamService) + @Inject(TeamType.services.GetTeamService) private getTeamService: GetTeamServiceInterface, + @Inject(TYPES.services.GetBoardService) + private getBoardService: GetBoardServiceInterface, @Inject(SchedulesType.TYPES.services.CreateSchedulesService) - private createSchedulesService: CreateSchedulesServiceInterface + private createSchedulesService: CreateSchedulesServiceInterface, + @Inject(CommunicationsType.TYPES.services.ExecuteCommunication) + private slackCommunicationService: ExecuteCommunicationInterface ) {} saveBoardUsers(newUsers: BoardUserDto[], newBoardId: string) { @@ -133,6 +147,20 @@ export default class CreateBoardServiceImpl implements CreateBoardService { this.createFirstCronJob(addCronJobDto); } + this.logger.verbose(`Communication Slack Enable is set to "${boardData.slackEnable}".`); + if (boardData.slackEnable) { + const result = await this.getBoardService.getBoard(newBoard._id, userId); + if (result?.board) { + this.logger.verbose(`Call Slack Communication Service for board id "${newBoard._id}".`); + const board = fillDividedBoardsUsersWithTeamUsers(translateBoard(result.board)); + this.slackCommunicationService.execute(board); + } else { + this.logger.error( + `Call Slack Communication Service for board id "${newBoard._id}" fails. Board not found.` + ); + } + } + return newBoard; } diff --git a/backend/src/modules/cards/services/create.card.service.ts b/backend/src/modules/cards/services/create.card.service.ts index fa4804c4b..807895274 100644 --- a/backend/src/modules/cards/services/create.card.service.ts +++ b/backend/src/modules/cards/services/create.card.service.ts @@ -17,7 +17,7 @@ export default class CreateCardServiceImpl implements CreateCardService { card.createdBy = userId; if (isEmpty(card.items)) { - card.items.push({ + (card.items as any[]).push({ text: card.text, createdBy: userId, comments: [], diff --git a/backend/src/modules/communication/applications/slack-execute-communication.application.ts b/backend/src/modules/communication/applications/slack-execute-communication.application.ts index 8f27aed1f..7bb4a0bf8 100644 --- a/backend/src/modules/communication/applications/slack-execute-communication.application.ts +++ b/backend/src/modules/communication/applications/slack-execute-communication.application.ts @@ -109,9 +109,12 @@ export class SlackExecuteCommunication implements ExecuteCommunicationInterface } private async createAllChannels(teams: TeamDto[]): Promise { + const today = new Date(); + const year = today.getFullYear(); + const month = today.toLocaleString('default', { month: 'long' }).toLowerCase(); const createChannelsPromises = teams.map((i) => this.conversationsHandler.createChannel( - `${i.normalName}${i.for === BoardRoles.RESPONSIBLE ? '-responsibles' : ''}` + `${i.normalName}${i.for === BoardRoles.RESPONSIBLE ? '-responsibles' : ''}-${month}-${year}` ) ); @@ -121,10 +124,10 @@ export class SlackExecuteCommunication implements ExecuteCommunicationInterface errors.forEach((i) => this.logger.warn(i)); success.forEach(({ id: channelId, name: channelName }) => { - const board = teams.find( - (i) => - channelName === + const board = teams.find((i) => + channelName.includes( `${i.normalName}${i.for === BoardRoles.RESPONSIBLE ? '-responsibles' : ''}` + ) ); if (board) { board.channelId = channelId; diff --git a/backend/src/modules/communication/communication.module.ts b/backend/src/modules/communication/communication.module.ts index 7a6026c80..75cecf32a 100644 --- a/backend/src/modules/communication/communication.module.ts +++ b/backend/src/modules/communication/communication.module.ts @@ -1,5 +1,5 @@ import { BullModule } from '@nestjs/bull'; -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { join } from 'path'; import BoardsModule from 'modules/boards/boards.module'; @@ -8,14 +8,14 @@ import { CommunicationGateAdapter, ConversationsHandler, ExecuteCommunication, + ExecuteCommunicationService, UsersHandler } from 'modules/communication/communication.providers'; import { CommunicationProducerService } from 'modules/communication/producers/slack-communication.producer.service'; -import { SlackExecuteCommunicationService } from 'modules/communication/services/slack-execute-communication.service'; @Module({ imports: [ - BoardsModule, + forwardRef(() => BoardsModule), BullModule.registerQueueAsync({ name: CommunicationProducerService.QUEUE_NAME, useFactory: async () => ({ @@ -25,7 +25,7 @@ import { SlackExecuteCommunicationService } from 'modules/communication/services }) ], providers: [ - SlackExecuteCommunicationService, + ExecuteCommunicationService, CommunicationGateAdapter, ChatHandler, ConversationsHandler, @@ -33,6 +33,6 @@ import { SlackExecuteCommunicationService } from 'modules/communication/services ExecuteCommunication, CommunicationProducerService ], - exports: [SlackExecuteCommunicationService] + exports: [ExecuteCommunicationService] }) export class CommunicationModule {} diff --git a/backend/src/modules/communication/communication.providers.ts b/backend/src/modules/communication/communication.providers.ts index e8453a4b2..d75db368e 100644 --- a/backend/src/modules/communication/communication.providers.ts +++ b/backend/src/modules/communication/communication.providers.ts @@ -14,7 +14,9 @@ import { UsersSlackHandler } from 'modules/communication/handlers/users-slack.ha import { ChatHandlerInterface } from 'modules/communication/interfaces/chat.handler.interface'; import { CommunicationGateInterface } from 'modules/communication/interfaces/communication-gate.interface'; import { ConversationsHandlerInterface } from 'modules/communication/interfaces/conversations.handler.interface'; +import { TYPES } from 'modules/communication/interfaces/types'; import { UsersHandlerInterface } from 'modules/communication/interfaces/users.handler.interface'; +import { SlackExecuteCommunicationService } from 'modules/communication/services/slack-execute-communication.service'; export const CommunicationGateAdapter = { provide: SlackCommunicationGateAdapter, @@ -75,3 +77,8 @@ export const ExecuteCommunication = { }, inject: [ConfigService, ConversationsSlackHandler, UsersSlackHandler, ChatSlackHandler] }; + +export const ExecuteCommunicationService = { + provide: TYPES.services.ExecuteCommunication, + useClass: SlackExecuteCommunicationService +}; diff --git a/backend/src/modules/communication/interfaces/types.ts b/backend/src/modules/communication/interfaces/types.ts new file mode 100644 index 000000000..d4b2c88ab --- /dev/null +++ b/backend/src/modules/communication/interfaces/types.ts @@ -0,0 +1,5 @@ +export const TYPES = { + services: { + ExecuteCommunication: 'ExecuteCommunication' + } +}; diff --git a/backend/src/modules/communication/producers/slack-communication.producer.service.ts b/backend/src/modules/communication/producers/slack-communication.producer.service.ts index 5a3de39d4..cf17595a1 100644 --- a/backend/src/modules/communication/producers/slack-communication.producer.service.ts +++ b/backend/src/modules/communication/producers/slack-communication.producer.service.ts @@ -1,6 +1,7 @@ import { InjectQueue } from '@nestjs/bull'; import { Injectable, Logger } from '@nestjs/common'; import { Job, Queue } from 'bull'; +import { createWriteStream } from 'fs'; import { JobType } from 'modules/communication/dto/types'; @@ -15,8 +16,9 @@ export class CommunicationProducerService { private readonly queue: Queue ) { // https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md#events - this.queue.on('completed', (job: Job) => { + this.queue.on('completed', (job: Job, result: any) => { this.logger.verbose(`Completed Job id: "${job.id}"`); + this.saveLog(result); job.remove(); }); this.queue.on('error', (error: Error) => { @@ -37,4 +39,14 @@ export class CommunicationProducerService { return job; } + + saveLog(data: any) { + try { + const stream = createWriteStream('slackLog.txt', { flags: 'a' }); + stream.write(`${JSON.stringify(data)}\n`); + stream.end(); + } catch (error) { + this.logger.error(error); + } + } } diff --git a/backend/src/test/boards/boards.controller.spec.ts b/backend/src/test/boards/boards.controller.spec.ts index dd5959427..a82dc10e3 100644 --- a/backend/src/test/boards/boards.controller.spec.ts +++ b/backend/src/test/boards/boards.controller.spec.ts @@ -13,6 +13,7 @@ import { updateBoardService } from 'modules/boards/boards.providers'; import BoardsController from 'modules/boards/controller/boards.controller'; +import * as CommunicationsType from 'modules/communication/interfaces/types'; import { createSchedulesService } from 'modules/schedules/schedules.providers'; import SocketGateway from 'modules/socket/gateway/socket.gateway'; import { createTeamService, getTeamApplication, getTeamService } from 'modules/teams/providers'; @@ -61,6 +62,12 @@ describe('BoardsController', () => { { provide: getModelToken('Schedules'), useValue: {} + }, + { + provide: CommunicationsType.TYPES.services.ExecuteCommunication, + useValue: { + execute: jest.fn() + } } ] }).compile(); diff --git a/frontend/src/components/Board/Settings/index.tsx b/frontend/src/components/Board/Settings/index.tsx index e8a482144..fac8ba4c7 100644 --- a/frontend/src/components/Board/Settings/index.tsx +++ b/frontend/src/components/Board/Settings/index.tsx @@ -1,10 +1,10 @@ -import React, { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; -import { useRecoilValue } from 'recoil'; import { joiResolver } from '@hookform/resolvers/joi'; import { Accordion } from '@radix-ui/react-accordion'; import { Dialog, DialogClose, DialogTrigger } from '@radix-ui/react-dialog'; import { deepClone } from 'fast-json-patch'; +import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useRecoilValue } from 'recoil'; import Icon from 'components/icons/Icon'; import Avatar from 'components/Primitives/Avatar'; @@ -225,7 +225,6 @@ const BoardSettings = ({ const keyDownHandler = (event: KeyboardEvent) => { if (event.key === 'Enter') { - console.log('---'); event.preventDefault(); if (submitBtnRef.current) { diff --git a/frontend/src/components/CreateBoard/SubTeamsTab/MainBoardCard.tsx b/frontend/src/components/CreateBoard/SubTeamsTab/MainBoardCard.tsx index 055fce893..9b3abb62d 100644 --- a/frontend/src/components/CreateBoard/SubTeamsTab/MainBoardCard.tsx +++ b/frontend/src/components/CreateBoard/SubTeamsTab/MainBoardCard.tsx @@ -70,13 +70,6 @@ const MainBoardCard = React.memo(({ team, timesOpen }: MainBoardCardInterface) = teamMembers } = useCreateBoard(team); - const slackGroupHandler = () => { - setCreateBoardData((prev) => ({ - ...prev, - board: { ...prev.board, slackGroup: !board.slackGroup } - })); - }; - const teamMembersCount = teamMembers?.length ?? 0; useEffect(() => { @@ -208,10 +201,11 @@ const MainBoardCard = React.memo(({ team, timesOpen }: MainBoardCardInterface) = - + + {/* onClick={slackEnableHandler} */} diff --git a/frontend/src/components/Primitives/Checkbox.tsx b/frontend/src/components/Primitives/Checkbox.tsx index 04be873c8..ca72cdf2d 100644 --- a/frontend/src/components/Primitives/Checkbox.tsx +++ b/frontend/src/components/Primitives/Checkbox.tsx @@ -1,6 +1,7 @@ -import React, { Dispatch, useState } from 'react'; import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; import { styled } from '@stitches/react'; +import React, { Dispatch, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; import Icon from 'components/icons/Icon'; import Flex from './Flex'; @@ -77,6 +78,8 @@ const Checkbox: React.FC<{ setCheckedTerms: null }; + const { setValue } = useFormContext(); + const [currentCheckValue, setCurrentCheckValue] = useState< boolean | undefined | 'indeterminate' >(checked); @@ -84,6 +87,8 @@ const Checkbox: React.FC<{ if (handleChange) handleChange(id); setCurrentCheckValue(isChecked); if (setCheckedTerms != null) setCheckedTerms(!!isChecked); + + setValue('slackEnable', !!isChecked); }; return ( diff --git a/frontend/src/hooks/useBoard.tsx b/frontend/src/hooks/useBoard.tsx index ca8537963..2b120df2c 100644 --- a/frontend/src/hooks/useBoard.tsx +++ b/frontend/src/hooks/useBoard.tsx @@ -1,6 +1,6 @@ +import { AxiosError } from 'axios'; import { useMutation, useQuery } from 'react-query'; import { useSetRecoilState } from 'recoil'; -import { AxiosError } from 'axios'; import { createBoardRequest, diff --git a/frontend/src/pages/boards/new.tsx b/frontend/src/pages/boards/new.tsx index 0de0c452a..4bfcf5084 100644 --- a/frontend/src/pages/boards/new.tsx +++ b/frontend/src/pages/boards/new.tsx @@ -1,10 +1,10 @@ +import { joiResolver } from '@hookform/resolvers/joi'; +import { GetServerSideProps, GetServerSidePropsContext, NextPage } from 'next'; +import { useRouter } from 'next/router'; import { useCallback, useEffect } from 'react'; import { FormProvider, useForm, useWatch } from 'react-hook-form'; import { dehydrate, QueryClient } from 'react-query'; -import { GetServerSideProps, GetServerSidePropsContext, NextPage } from 'next'; -import { useRouter } from 'next/router'; import { useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil'; -import { joiResolver } from '@hookform/resolvers/joi'; import { ButtonsContainer, @@ -54,12 +54,13 @@ const NewBoard: NextPage = () => { /** * React Hook Form */ - const methods = useForm<{ text: string; maxVotes?: number }>({ + const methods = useForm<{ text: string; maxVotes?: number; slackEnable?: boolean }>({ mode: 'onBlur', reValidateMode: 'onBlur', defaultValues: { text: '', - maxVotes: boardState.board.maxVotes + maxVotes: boardState.board.maxVotes, + slackEnable: false }, resolver: joiResolver(SchemaCreateBoard) }); @@ -82,7 +83,7 @@ const NewBoard: NextPage = () => { * @param title Board Title * @param maxVotes Maxium number of votes allowed */ - const saveBoard = (title: string, maxVotes?: number) => { + const saveBoard = (title: string, maxVotes?: number, slackEnable?: boolean) => { const newDividedBoards: CreateBoardDto[] = boardState.board.dividedBoards.map( (subBoard) => { const newSubBoard: CreateBoardDto = { ...subBoard, users: [], dividedBoards: [] }; @@ -105,6 +106,7 @@ const NewBoard: NextPage = () => { title, dividedBoards: newDividedBoards, maxVotes, + slackEnable, maxUsers: boardState.count.maxUsersCount.toString() }); }; @@ -151,9 +153,9 @@ const NewBoard: NextPage = () => { status={!haveError} onSubmit={ !haveError - ? methods.handleSubmit(({ text, maxVotes }) => - saveBoard(text, maxVotes) - ) + ? methods.handleSubmit(({ text, maxVotes, slackEnable }) => { + saveBoard(text, maxVotes, slackEnable); + }) : undefined } > diff --git a/frontend/src/schema/schemaCreateBoardForm.ts b/frontend/src/schema/schemaCreateBoardForm.ts index 87b901361..ec6155581 100644 --- a/frontend/src/schema/schemaCreateBoardForm.ts +++ b/frontend/src/schema/schemaCreateBoardForm.ts @@ -8,6 +8,9 @@ const SchemaCreateBoard = Joi.object({ }), maxVotes: Joi.number().min(1).optional().messages({ 'number.min': 'Please insert a number greater than zero.' + }), + slackEnable: Joi.boolean().required().messages({ + 'boolean.required': 'Please enterm the value for slack enable.' }) }); diff --git a/frontend/src/store/createBoard/atoms/create-board.atom.tsx b/frontend/src/store/createBoard/atoms/create-board.atom.tsx index 2afa17dda..5ff0dfe38 100644 --- a/frontend/src/store/createBoard/atoms/create-board.atom.tsx +++ b/frontend/src/store/createBoard/atoms/create-board.atom.tsx @@ -50,7 +50,7 @@ export const createBoardDataState = atom({ isSubBoard: false, hideCards: false, hideVotes: false, - slackGroup: false, + slackEnable: false, totalUsedVotes: 0 } } diff --git a/frontend/src/types/board/board.ts b/frontend/src/types/board/board.ts index 0e7e52903..33d4b4a59 100644 --- a/frontend/src/types/board/board.ts +++ b/frontend/src/types/board/board.ts @@ -38,7 +38,7 @@ export default interface BoardType { votes?: string; submitedByUser?: string; submitedAt?: Date; - slackGroup?: boolean; + slackEnable?: boolean; totalUsedVotes: number; }