Skip to content

Commit

Permalink
N21-2319 Change board type (#5442)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarvinOehlerkingCap authored Jan 22, 2025
1 parent 808fca2 commit a580ad0
Show file tree
Hide file tree
Showing 15 changed files with 371 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { EntityManager } from '@mikro-orm/mongodb';
import { accountFactory } from '@modules/account/testing';
import { GroupEntityTypes } from '@modules/group/entity';
import { roomMembershipEntityFactory } from '@modules/room-membership/testing';
import { roomEntityFactory } from '@modules/room/testing';
import { ServerTestModule } from '@modules/server/server.app.module';
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { Permission, RoleName } from '@shared/domain/interface';
import { cleanupCollections } from '@testing/cleanup-collections';
import { groupEntityFactory } from '@testing/factory/group-entity.factory';
import { roleFactory } from '@testing/factory/role.factory';
import { userFactory } from '@testing/factory/user.factory';
import { TestApiClient } from '@testing/test-api-client';
import { BoardExternalReferenceType, BoardLayout } from '../../domain';
import { BoardNodeEntity } from '../../repo';
import { columnBoardEntityFactory } from '../../testing';

const baseRouteName = '/boards';

describe(`board update layout with room relation (api)`, () => {
let app: INestApplication;
let em: EntityManager;
let testApiClient: TestApiClient;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [ServerTestModule],
}).compile();

app = module.createNestApplication();
await app.init();
em = module.get(EntityManager);
testApiClient = new TestApiClient(app, baseRouteName);
});

afterAll(async () => {
await app.close();
});

beforeEach(async () => {
await cleanupCollections(em);
});

const setup = async () => {
const userWithEditRole = userFactory.buildWithId();
const accountWithEditRole = accountFactory.withUser(userWithEditRole).build();

const userWithViewRole = userFactory.buildWithId();
const accountWithViewRole = accountFactory.withUser(userWithViewRole).build();

const noAccessUser = userFactory.buildWithId();
const noAccessAccount = accountFactory.withUser(noAccessUser).build();

const roleRoomEdit = roleFactory.buildWithId({
name: RoleName.ROOMEDITOR,
permissions: [Permission.ROOM_EDIT],
});
const roleRoomView = roleFactory.buildWithId({
name: RoleName.ROOMVIEWER,
permissions: [Permission.ROOM_VIEW],
});

const userGroup = groupEntityFactory.buildWithId({
type: GroupEntityTypes.ROOM,
users: [
{ user: userWithEditRole, role: roleRoomEdit },
{ user: userWithViewRole, role: roleRoomView },
],
});

const room = roomEntityFactory.buildWithId();

const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id });

await em.persistAndFlush([
accountWithEditRole,
accountWithViewRole,
noAccessAccount,
userWithEditRole,
userWithViewRole,
noAccessUser,
roleRoomEdit,
roleRoomView,
userGroup,
room,
roomMembership,
]);

const columnBoardNode = columnBoardEntityFactory.build({
layout: BoardLayout.COLUMNS,
context: { id: room.id, type: BoardExternalReferenceType.Room },
});

await em.persistAndFlush([columnBoardNode]);
em.clear();

return { accountWithEditRole, accountWithViewRole, noAccessAccount, columnBoardNode };
};

describe('with user who has edit role in room', () => {
it('should return status 204', async () => {
const { accountWithEditRole, columnBoardNode } = await setup();
const loggedInClient = await testApiClient.login(accountWithEditRole);

const response = await loggedInClient.patch(`${columnBoardNode.id}/layout`, { layout: BoardLayout.LIST });

expect(response.status).toEqual(204);
});

it('should actually change the board layout', async () => {
const { accountWithEditRole, columnBoardNode } = await setup();
const loggedInClient = await testApiClient.login(accountWithEditRole);

await loggedInClient.patch(`${columnBoardNode.id}/layout`, { layout: BoardLayout.LIST });

const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id);

expect(result.layout).toEqual(BoardLayout.LIST);
});
});

describe('with user who has only view role in room', () => {
it('should return status 403', async () => {
const { accountWithViewRole, columnBoardNode } = await setup();
const loggedInClient = await testApiClient.login(accountWithViewRole);

const response = await loggedInClient.patch(`${columnBoardNode.id}/layout`, { layout: BoardLayout.LIST });

expect(response.status).toEqual(403);
});

it('should not change the board layout', async () => {
const { accountWithViewRole, columnBoardNode } = await setup();
const loggedInClient = await testApiClient.login(accountWithViewRole);

await loggedInClient.patch(`${columnBoardNode.id}/layout`, { layout: BoardLayout.LIST });

const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id);
expect(result.layout).toEqual(BoardLayout.COLUMNS);
});
});

describe('with user who is not part of the room', () => {
it('should return status 403', async () => {
const { noAccessAccount, columnBoardNode } = await setup();
const loggedInClient = await testApiClient.login(noAccessAccount);

const response = await loggedInClient.patch(`${columnBoardNode.id}/layout`, { layout: BoardLayout.LIST });

expect(response.status).toEqual(403);
});

it('should not change the board layout', async () => {
const { noAccessAccount, columnBoardNode } = await setup();
const loggedInClient = await testApiClient.login(noAccessAccount);

await loggedInClient.patch(`${columnBoardNode.id}/layout`, { layout: BoardLayout.LIST });

const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id);
expect(result.layout).toEqual(BoardLayout.COLUMNS);
});
});
});
16 changes: 16 additions & 0 deletions apps/server/src/modules/board/controller/board.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
ColumnResponse,
CreateBoardBodyParams,
CreateBoardResponse,
LayoutBodyParams,
UpdateBoardTitleParams,
VisibilityBodyParams,
} from './dto';
Expand Down Expand Up @@ -157,4 +158,19 @@ export class BoardController {
) {
await this.boardUc.updateVisibility(currentUser.userId, urlParams.boardId, bodyParams.isVisible);
}

@ApiOperation({ summary: 'Update the layout of a board.' })
@ApiResponse({ status: 204 })
@ApiResponse({ status: 400, type: ApiValidationError })
@ApiResponse({ status: 403, type: ForbiddenException })
@ApiResponse({ status: 404, type: NotFoundException })
@HttpCode(204)
@Patch(':boardId/layout')
public async updateLayout(
@Param() urlParams: BoardUrlParams,
@Body() bodyParams: LayoutBodyParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<void> {
await this.boardUc.updateLayout(currentUser.userId, urlParams.boardId, bodyParams.layout);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './move-column.body.params';
export * from './rename.body.params';
export * from './update-board-title.body.params';
export * from './visibility.body.params';
export * from './layout.body.params';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum } from 'class-validator';
import { BoardLayout } from '../../../domain';

export class LayoutBodyParams {
@IsEnum(BoardLayout)
@ApiProperty({ enum: BoardLayout, enumName: 'BoardLayout' })
public layout!: BoardLayout;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import { BoardLayout } from '../../../domain/types';
export class LayoutBodyParams {
@IsEnum(BoardLayout)
@NotEquals(BoardLayout[BoardLayout.COLUMNS])
@ApiProperty({ enum: BoardLayout, enumName: 'MediaBoardLayoutType' })
layout!: BoardLayout;
@ApiProperty({ enum: BoardLayout, enumName: 'BoardLayout' })
public layout!: BoardLayout;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class MediaBoardResponse {

@ApiProperty({
enum: BoardLayout,
enumName: 'MediaBoardLayoutType',
enumName: 'BoardLayout',
description: 'Layout of media board',
})
layout: BoardLayout;
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/modules/board/domain/colum-board.do.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export class ColumnBoard extends BoardNode<ColumnBoardProps> {
return this.props.layout;
}

set layout(layout: BoardLayout) {
this.props.layout = layout;
}

canHaveChild(childNode: AnyBoardNode): boolean {
const allowed = childNode instanceof Column;
return allowed;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { MongoIoAdapter } from '@infra/socketio';
import { EntityManager } from '@mikro-orm/mongodb';
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';

import { MongoIoAdapter } from '@infra/socketio';
import { InputFormat } from '@shared/domain/types/input-format.types';
import { cleanupCollections } from '@testing/cleanup-collections';
import { courseFactory } from '@testing/factory/course.factory';
Expand All @@ -12,7 +11,7 @@ import { UserAndAccountTestFactory } from '@testing/factory/user-and-account.tes
import { getSocketApiClient, waitForEvent } from '@testing/test-socket-api-client';
import { Socket } from 'socket.io-client';
import { BoardCollaborationTestModule } from '../../board-collaboration.app.module';
import { BoardExternalReferenceType, CardProps, ContentElementType } from '../../domain';
import { BoardExternalReferenceType, BoardLayout, CardProps, ContentElementType } from '../../domain';
import {
cardEntityFactory,
columnBoardEntityFactory,
Expand Down Expand Up @@ -360,6 +359,32 @@ describe(BoardCollaborationGateway.name, () => {
});
});

describe('update board layout', () => {
describe('when board exists', () => {
it('should answer with success', async () => {
const { columnBoardNode } = await setup();
const boardId = columnBoardNode.id;

ioClient.emit('update-board-layout-request', { boardId, layout: BoardLayout.LIST });
const success = await waitForEvent(ioClient, 'update-board-layout-success');

expect(success).toEqual(expect.objectContaining({ boardId, layout: BoardLayout.LIST }));
});
});

describe('when user is not authorized', () => {
it('should answer with failure', async () => {
const { columnBoardNode } = await setup();
const boardId = columnBoardNode.id;

unauthorizedIoClient.emit('update-board-layout-request', { boardId, layout: BoardLayout.LIST });
const failure = await waitForEvent(unauthorizedIoClient, 'update-board-layout-failure');

expect(failure).toEqual({ boardId, layout: BoardLayout.LIST });
});
});
});

describe('delete column', () => {
describe('when column exists', () => {
it('should answer with success', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,32 @@ import {
ColumnResponseMapper,
ContentElementResponseFactory,
} from '../controller/mapper';
import { AnyBoardNode } from '../domain';
import { AnyBoardNode, ColumnBoard } from '../domain';
import { MetricsService } from '../metrics/metrics.service';
import { TrackExecutionTime } from '../metrics/track-execution-time.decorator';
import { BoardUc, CardUc, ColumnUc, ElementUc } from '../uc';
import {
CreateCardMessageParams,
CreateColumnMessageParams,
CreateContentElementMessageParams,
DeleteBoardMessageParams,
DeleteCardMessageParams,
DeleteColumnMessageParams,
DeleteContentElementMessageParams,
FetchBoardMessageParams,
FetchCardsMessageParams,
MoveCardMessageParams,
MoveColumnMessageParams,
MoveContentElementMessageParams,
UpdateBoardLayoutMessageParams,
UpdateBoardTitleMessageParams,
UpdateBoardVisibilityMessageParams,
UpdateCardHeightMessageParams,
UpdateCardTitleMessageParams,
UpdateColumnTitleMessageParams,
UpdateContentElementMessageParams,
} from './dto';
import BoardCollaborationConfiguration from './dto/board-collaboration-config';
import { CreateCardMessageParams } from './dto/create-card.message.param';
import { CreateColumnMessageParams } from './dto/create-column.message.param';
import { CreateContentElementMessageParams } from './dto/create-content-element.message.param';
import { DeleteBoardMessageParams } from './dto/delete-board.message.param';
import { DeleteCardMessageParams } from './dto/delete-card.message.param';
import { DeleteColumnMessageParams } from './dto/delete-column.message.param';
import { DeleteContentElementMessageParams } from './dto/delete-content-element.message.param';
import { FetchBoardMessageParams } from './dto/fetch-board.message.param';
import { FetchCardsMessageParams } from './dto/fetch-cards.message.param';
import { MoveCardMessageParams } from './dto/move-card.message.param';
import { MoveColumnMessageParams } from './dto/move-column.message.param';
import { MoveContentElementMessageParams } from './dto/move-content-element.message.param';
import { UpdateBoardTitleMessageParams } from './dto/update-board-title.message.param';
import { UpdateBoardVisibilityMessageParams } from './dto/update-board-visibility.message.param';
import { UpdateCardHeightMessageParams } from './dto/update-card-height.message.param';
import { UpdateCardTitleMessageParams } from './dto/update-card-title.message.param';
import { UpdateColumnTitleMessageParams } from './dto/update-column-title.message.param';
import { UpdateContentElementMessageParams } from './dto/update-content-element.message.param';

@UsePipes(new WsValidationPipe())
@WebSocketGateway(BoardCollaborationConfiguration.websocket)
Expand Down Expand Up @@ -291,6 +294,21 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect {
await this.updateRoomsAndUsersMetrics(socket);
}

@SubscribeMessage('update-board-layout-request')
@TrackExecutionTime()
@UseRequestContext()
public async updateBoardLayout(socket: Socket, data: UpdateBoardLayoutMessageParams): Promise<void> {
const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-board-layout' });
const { userId } = this.getCurrentUser(socket);
try {
const board: ColumnBoard = await this.boardUc.updateLayout(userId, data.boardId, data.layout);
emitter.emitToClientAndRoom(data, board);
} catch (err) {
emitter.emitFailure(data);
}
await this.updateRoomsAndUsersMetrics(socket);
}

@SubscribeMessage('delete-column-request')
@TrackExecutionTime()
@UseRequestContext()
Expand Down
Loading

0 comments on commit a580ad0

Please sign in to comment.