Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

N21-2319 Change board type #5442

Merged
merged 7 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading