From 92fa549469aef42649e7f0e857f60aede8e91bfa Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Wed, 10 Apr 2024 18:59:12 +0200 Subject: [PATCH 01/12] extend board creation api --- .../api-test/board-create.api.spec.ts | 101 +++++++++++++++++- .../controller/dto/board/board.response.ts | 7 +- .../dto/board/create-board.body.params.ts | 15 ++- .../mapper/board-response.mapper.ts | 3 +- .../board/types/board-layout.enum.ts | 4 + .../domain/domainobject/board/types/index.ts | 1 + .../boardnode/column-board-node.entity.ts | 4 + 7 files changed, 126 insertions(+), 9 deletions(-) create mode 100644 apps/server/src/shared/domain/domainobject/board/types/board-layout.enum.ts diff --git a/apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts index 03bb025ed7e..f747879f91f 100644 --- a/apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts @@ -2,7 +2,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server/server.module'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; +import { BoardExternalReferenceType, BoardLayout } from '@shared/domain/domainobject'; import { ColumnBoardNode } from '@shared/domain/entity'; import { TestApiClient, UserAndAccountTestFactory, cleanupCollections, courseFactory } from '@shared/testing'; @@ -46,7 +46,7 @@ describe(`create board (api)`, () => { return { loggedInClient, course }; }; - it('should return status 204 and board', async () => { + it('should return status 201 and board', async () => { const { loggedInClient, course } = await setup(); const title = 'new board'; @@ -63,6 +63,70 @@ describe(`create board (api)`, () => { const dbResult = await em.findOneOrFail(ColumnBoardNode, boardId); expect(dbResult.title).toEqual(title); }); + + describe('Board layout', () => { + describe('When layout is omitted', () => { + it('should create a column board', async () => { + const { loggedInClient, course } = await setup(); + const title = 'new board'; + + const response = await loggedInClient.post(undefined, { + title, + parentId: course.id, + parentType: BoardExternalReferenceType.Course, + }); + + const boardId = (response.body as { id: string }).id; + expect(response.status).toEqual(201); + expect(boardId).toBeDefined(); + + const dbResult = await em.findOneOrFail(ColumnBoardNode, boardId); + expect(dbResult.layout).toBeUndefined(); + }); + }); + + describe(`When layout is set to "${BoardLayout.COLUMNS}"`, () => { + it('should create a column board', async () => { + const { loggedInClient, course } = await setup(); + const title = 'new board'; + + const response = await loggedInClient.post(undefined, { + title, + parentId: course.id, + parentType: BoardExternalReferenceType.Course, + layout: BoardLayout.COLUMNS, + }); + + const boardId = (response.body as { id: string }).id; + expect(response.status).toEqual(201); + expect(boardId).toBeDefined(); + + const dbResult = await em.findOneOrFail(ColumnBoardNode, boardId); + expect(dbResult.layout).toBeUndefined(); + }); + }); + + describe(`When layout is set to "${BoardLayout.LIST}"`, () => { + it('should create a list board', async () => { + const { loggedInClient, course } = await setup(); + const title = 'new board'; + + const response = await loggedInClient.post(undefined, { + title, + parentId: course.id, + parentType: BoardExternalReferenceType.Course, + layout: BoardLayout.LIST, + }); + + const boardId = (response.body as { id: string }).id; + expect(response.status).toEqual(201); + expect(boardId).toBeDefined(); + + const dbResult = await em.findOneOrFail(ColumnBoardNode, boardId); + expect(dbResult.layout).toEqual(BoardLayout.LIST); + }); + }); + }); }); describe('When user is teacher and has no course permission', () => { @@ -78,7 +142,7 @@ describe(`create board (api)`, () => { return { loggedInClient, course }; }; - it('should return status 204 and board', async () => { + it('should return status 403', async () => { const { loggedInClient, course } = await setup(); const title = 'new board'; @@ -92,7 +156,7 @@ describe(`create board (api)`, () => { }); }); - describe('When user is student and has course permission', () => { + describe('When user is student and has no course permission', () => { const setup = async () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); @@ -105,7 +169,7 @@ describe(`create board (api)`, () => { return { loggedInClient, course }; }; - it('should return status 204 and board', async () => { + it('should return status 403', async () => { const { loggedInClient, course } = await setup(); const title = 'new board'; @@ -227,5 +291,32 @@ describe(`create board (api)`, () => { expect(response.status).toEqual(400); }); }); + + describe('When layout is invalid', () => { + const setup = async () => { + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + + const course = courseFactory.build({ teachers: [teacherUser] }); + await em.persistAndFlush([teacherUser, course, teacherAccount]); + em.clear(); + + const loggedInClient = await testApiClient.login(teacherAccount); + + return { loggedInClient, course }; + }; + it('should return status 400', async () => { + const { loggedInClient, course } = await setup(); + const title = 'new board'; + + const response = await loggedInClient.post(undefined, { + title, + parentId: course.id, + parentType: BoardExternalReferenceType.Course, + layout: 'invalid', + }); + + expect(response.status).toEqual(400); + }); + }); }); }); diff --git a/apps/server/src/modules/board/controller/dto/board/board.response.ts b/apps/server/src/modules/board/controller/dto/board/board.response.ts index c36ddf657c8..c8cb5128ed0 100644 --- a/apps/server/src/modules/board/controller/dto/board/board.response.ts +++ b/apps/server/src/modules/board/controller/dto/board/board.response.ts @@ -1,15 +1,17 @@ import { ApiProperty } from '@nestjs/swagger'; import { DecodeHtmlEntities } from '@shared/controller'; +import { BoardLayout } from '@shared/domain/domainobject'; import { ColumnResponse } from './column.response'; import { TimestampsResponse } from '../timestamps.response'; export class BoardResponse { - constructor({ id, title, columns, timestamps, isVisible }: BoardResponse) { + constructor({ id, title, columns, timestamps, isVisible, layout }: BoardResponse) { this.id = id; this.title = title; this.columns = columns; this.timestamps = timestamps; this.isVisible = isVisible; + this.layout = layout; } @ApiProperty({ @@ -31,4 +33,7 @@ export class BoardResponse { @ApiProperty() isVisible: boolean; + + @ApiProperty() + layout: BoardLayout; } diff --git a/apps/server/src/modules/board/controller/dto/board/create-board.body.params.ts b/apps/server/src/modules/board/controller/dto/board/create-board.body.params.ts index 08f70761ef4..495b2d74a70 100644 --- a/apps/server/src/modules/board/controller/dto/board/create-board.body.params.ts +++ b/apps/server/src/modules/board/controller/dto/board/create-board.body.params.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { SanitizeHtml } from '@shared/controller'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { IsEnum, IsMongoId, MaxLength, MinLength } from 'class-validator'; +import { BoardExternalReferenceType, BoardLayout } from '@shared/domain/domainobject'; +import { IsEnum, IsMongoId, MaxLength, MinLength, ValidateIf } from 'class-validator'; export class CreateBoardBodyParams { @ApiProperty({ @@ -28,4 +28,15 @@ export class CreateBoardBodyParams { }) @IsEnum(BoardExternalReferenceType) parentType!: BoardExternalReferenceType; + + @ApiProperty({ + description: 'The layout of the board', + required: false, + default: BoardLayout.COLUMNS, + enum: BoardLayout, + enumName: 'BoardLayout', + }) + @ValidateIf((o: CreateBoardBodyParams) => o.layout !== undefined) + @IsEnum(BoardLayout, {}) + layout?: BoardLayout; } diff --git a/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts index 4c951d047ac..ac833acd0e7 100644 --- a/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts @@ -1,5 +1,5 @@ import { HttpException, HttpStatus } from '@nestjs/common'; -import { Column, ColumnBoard } from '@shared/domain/domainobject'; +import { BoardLayout, Column, ColumnBoard } from '@shared/domain/domainobject'; import { BoardResponse, TimestampsResponse } from '../dto'; import { ColumnResponseMapper } from './column-response.mapper'; @@ -20,6 +20,7 @@ export class BoardResponseMapper { }), timestamps: new TimestampsResponse({ lastUpdatedAt: board.updatedAt, createdAt: board.createdAt }), isVisible: board.isVisible, + layout: BoardLayout.COLUMNS, // TODO map from domain object }); return result; } diff --git a/apps/server/src/shared/domain/domainobject/board/types/board-layout.enum.ts b/apps/server/src/shared/domain/domainobject/board/types/board-layout.enum.ts new file mode 100644 index 00000000000..9d41bde2592 --- /dev/null +++ b/apps/server/src/shared/domain/domainobject/board/types/board-layout.enum.ts @@ -0,0 +1,4 @@ +export enum BoardLayout { + COLUMNS = 'columns', + LIST = 'list', +} diff --git a/apps/server/src/shared/domain/domainobject/board/types/index.ts b/apps/server/src/shared/domain/domainobject/board/types/index.ts index 0c3298673a0..8c080ffa4a9 100644 --- a/apps/server/src/shared/domain/domainobject/board/types/index.ts +++ b/apps/server/src/shared/domain/domainobject/board/types/index.ts @@ -7,3 +7,4 @@ export * from './board-do-authorizable'; export * from './board-external-reference'; export * from './column-board-info'; export * from './content-elements.enum'; +export * from './board-layout.enum'; diff --git a/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts index 8539fefe2a2..b757218e44c 100644 --- a/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts +++ b/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts @@ -4,6 +4,7 @@ import { AnyBoardDo, BoardExternalReference, BoardExternalReferenceType, + BoardLayout, } from '@shared/domain/domainobject/board/types'; import { LearnroomElement } from '../../interface'; import { BoardNode } from './boardnode.entity'; @@ -39,6 +40,9 @@ export class ColumnBoardNode extends BoardNode implements LearnroomElement { }; } + @Property({ nullable: true }) + layout?: BoardLayout; + useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { const domainObject = builder.buildColumnBoard(this); return domainObject; From d7ed260393b6e47cd920e7276a6fb759ac2452de Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Wed, 10 Apr 2024 20:57:47 +0200 Subject: [PATCH 02/12] add domain object property --- .../src/shared/domain/domainobject/board/column-board.do.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/server/src/shared/domain/domainobject/board/column-board.do.ts b/apps/server/src/shared/domain/domainobject/board/column-board.do.ts index de420852694..a4e1dfe127e 100644 --- a/apps/server/src/shared/domain/domainobject/board/column-board.do.ts +++ b/apps/server/src/shared/domain/domainobject/board/column-board.do.ts @@ -1,6 +1,7 @@ import { BoardComposite, BoardCompositeProps } from './board-composite.do'; import { Column } from './column.do'; import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync, BoardExternalReference } from './types'; +import { BoardLayout } from './types/board-layout.enum'; export class ColumnBoard extends BoardComposite { get title(): string { @@ -27,6 +28,10 @@ export class ColumnBoard extends BoardComposite { this.props.isVisible = isVisible; } + get layout(): BoardLayout { + return this.props.layout ?? BoardLayout.COLUMNS; + } + isAllowedAsChild(domainObject: AnyBoardDo): boolean { const allowed = domainObject instanceof Column; return allowed; @@ -45,6 +50,7 @@ export interface ColumnBoardProps extends BoardCompositeProps { title: string; context: BoardExternalReference; isVisible: boolean; + layout: BoardLayout; } export function isColumnBoard(reference: unknown): reference is ColumnBoard { From e8a1b9e1cf1c708a21b1c75a72fb83bb60d80a2b Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Wed, 10 Apr 2024 20:58:13 +0200 Subject: [PATCH 03/12] extend board lookup api --- .../api-test/board-lookup.api.spec.ts | 167 +++++++++--------- .../mapper/board-response.mapper.ts | 4 +- 2 files changed, 84 insertions(+), 87 deletions(-) diff --git a/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts index fc5b3e49ed7..37399ca4aa6 100644 --- a/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts @@ -1,140 +1,137 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ServerTestModule } from '@modules/server/server.module'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ApiValidationError } from '@shared/common'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; +import { BoardExternalReferenceType, BoardLayout } from '@shared/domain/domainobject'; import { cardNodeFactory, cleanupCollections, columnBoardNodeFactory, columnNodeFactory, courseFactory, - mapUserToCurrentUser, - userFactory, + TestApiClient, + UserAndAccountTestFactory, } from '@shared/testing'; -import { Request } from 'express'; -import request from 'supertest'; import { BoardResponse } from '../dto'; const baseRouteName = '/boards'; -class API { - app: INestApplication; - - constructor(app: INestApplication) { - this.app = app; - } - - async get(id: string) { - const response = await request(this.app.getHttpServer()) - .get(`${baseRouteName}/${id}`) - .set('Accept', 'application/json'); - - return { - result: response.body as BoardResponse, - error: response.body as ApiValidationError, - status: response.status, - }; - } -} - describe(`board lookup (api)`, () => { let app: INestApplication; let em: EntityManager; - let currentUser: ICurrentUser; - let api: API; + let testApiClient: TestApiClient; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], - }) - .overrideGuard(JwtAuthGuard) - .useValue({ - canActivate(context: ExecutionContext) { - const req: Request = context.switchToHttp().getRequest(); - req.user = currentUser; - return true; - }, - }) - .compile(); + }).compile(); app = module.createNestApplication(); await app.init(); em = module.get(EntityManager); - api = new API(app); + testApiClient = new TestApiClient(app, baseRouteName); }); afterAll(async () => { await app.close(); }); - const setup = async () => { + beforeEach(async () => { await cleanupCollections(em); - const user = userFactory.build(); - const course = courseFactory.build({ teachers: [user] }); - await em.persistAndFlush([user, course]); + }); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ - context: { id: course.id, type: BoardExternalReferenceType.Course }, - }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode1 = cardNodeFactory.buildWithId({ parent: columnNode }); - const cardNode2 = cardNodeFactory.buildWithId({ parent: columnNode }); - const cardNode3 = cardNodeFactory.buildWithId({ parent: columnNode }); - const notOfThisBoardCardNode = cardNodeFactory.buildWithId(); + describe('When user is course teacher', () => { + const setup = async () => { + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + + const course = courseFactory.build({ teachers: [teacherUser] }); + await em.persistAndFlush([teacherUser, course, teacherAccount]); + + const columnBoardNode = columnBoardNodeFactory.buildWithId({ + context: { id: course.id, type: BoardExternalReferenceType.Course }, + }); + const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const cardNode1 = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode2 = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode3 = cardNodeFactory.buildWithId({ parent: columnNode }); + const notOfThisBoardCardNode = cardNodeFactory.buildWithId(); + + await em.persistAndFlush([columnBoardNode, columnNode, cardNode1, cardNode2, cardNode3, notOfThisBoardCardNode]); + em.clear(); + + const loggedInClient = await testApiClient.login(teacherAccount); - await em.persistAndFlush([columnBoardNode, columnNode, cardNode1, cardNode2, cardNode3, notOfThisBoardCardNode]); - em.clear(); + return { loggedInClient, columnBoardNode, columnNode, card1: cardNode1, card2: cardNode2, card3: cardNode3 }; + }; + + describe('with valid board id', () => { + it('should return status 200', async () => { + const { loggedInClient, columnBoardNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const response = await loggedInClient.get(columnBoardNode.id); - return { columnBoardNode, columnNode, card1: cardNode1, card2: cardNode2, card3: cardNode3 }; - }; + expect(response.status).toEqual(200); + }); - describe('with valid board ids', () => { - it('should return status 200', async () => { - const { columnBoardNode } = await setup(); + it('should return the right board', async () => { + const { loggedInClient, columnBoardNode, columnNode } = await setup(); - const response = await api.get(`${columnBoardNode.id}`); + const response = await loggedInClient.get(columnBoardNode.id); + const result = response.body as BoardResponse; - expect(response.status).toEqual(200); + expect(result.id).toEqual(columnBoardNode.id); + expect(result.columns).toHaveLength(1); + expect(result.columns[0].id).toEqual(columnNode.id); + expect(result.columns[0].cards).toHaveLength(3); + }); }); - it('should return the right board', async () => { - const { columnBoardNode, columnNode } = await setup(); + describe('board layout', () => { + it(`should default to ${BoardLayout.COLUMNS}`, async () => { + const { loggedInClient, columnBoardNode } = await setup(); - const { result } = await api.get(columnBoardNode.id); + const response = await loggedInClient.get(columnBoardNode.id); + const result = response.body as BoardResponse; - expect(result.id).toEqual(columnBoardNode.id); - expect(result.columns).toHaveLength(1); - expect(result.columns[0].id).toEqual(columnNode.id); - expect(result.columns[0].cards).toHaveLength(3); + expect(result.layout).toEqual(BoardLayout.COLUMNS); + }); }); - }); - describe('with invalid board id', () => { - it('should return status 404', async () => { - await setup(); - const notExistingBoardId = new ObjectId().toString(); + describe('with invalid board id', () => { + it('should return status 404', async () => { + const { loggedInClient } = await setup(); + const notExistingBoardId = new ObjectId().toString(); - const response = await api.get(notExistingBoardId); + const response = await loggedInClient.get(notExistingBoardId); - expect(response.status).toEqual(404); + expect(response.status).toEqual(404); + }); }); }); - describe('with invalid user', () => { - it('should return status 403', async () => { - const { columnBoardNode } = await setup(); + describe('When user does not belong to course', () => { + const setup = async () => { + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + + const course = courseFactory.build(); + await em.persistAndFlush([teacherUser, course, teacherAccount]); + + const columnBoardNode = columnBoardNodeFactory.buildWithId({ + context: { id: course.id, type: BoardExternalReferenceType.Course }, + }); + await em.persistAndFlush([columnBoardNode]); - const invalidUser = userFactory.build(); - await em.persistAndFlush([invalidUser]); - currentUser = mapUserToCurrentUser(invalidUser); + em.clear(); + + const loggedInClient = await testApiClient.login(teacherAccount); + + return { loggedInClient, columnBoardNode }; + }; + + it('should return status 403', async () => { + const { loggedInClient, columnBoardNode } = await setup(); - const response = await api.get(`${columnBoardNode.id}`); + const response = await loggedInClient.get(columnBoardNode.id); expect(response.status).toEqual(403); }); diff --git a/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts index ac833acd0e7..74682cf1e25 100644 --- a/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts @@ -1,5 +1,5 @@ import { HttpException, HttpStatus } from '@nestjs/common'; -import { BoardLayout, Column, ColumnBoard } from '@shared/domain/domainobject'; +import { Column, ColumnBoard } from '@shared/domain/domainobject'; import { BoardResponse, TimestampsResponse } from '../dto'; import { ColumnResponseMapper } from './column-response.mapper'; @@ -20,7 +20,7 @@ export class BoardResponseMapper { }), timestamps: new TimestampsResponse({ lastUpdatedAt: board.updatedAt, createdAt: board.createdAt }), isVisible: board.isVisible, - layout: BoardLayout.COLUMNS, // TODO map from domain object + layout: board.layout, }); return result; } From dfc3061c787f24c576af4d22fa317a54ef9e3b99 Mon Sep 17 00:00:00 2001 From: MartinSchuhmacher Date: Fri, 12 Apr 2024 23:22:55 +0200 Subject: [PATCH 04/12] more additions for FE implementation --- apps/server/src/modules/board/repo/board-do.builder-impl.ts | 2 ++ .../server/src/modules/board/repo/recursive-save.visitor.ts | 1 + .../service/board-do-copy-service/recursive-copy.visitor.ts | 1 + .../src/modules/board/service/column-board.service.ts | 4 +++- apps/server/src/modules/board/uc/board.uc.ts | 6 ++++-- .../dto/single-column-board/board-column-board.response.ts | 6 +++++- .../modules/learnroom/mapper/room-board-response.mapper.ts | 1 + .../learnroom/service/common-cartridge-import.service.ts | 3 ++- apps/server/src/modules/learnroom/types/room-board.types.ts | 1 + .../src/modules/learnroom/uc/room-board-dto.factory.ts | 1 + .../src/shared/domain/domainobject/board/column-board.do.ts | 4 ++++ .../domain/entity/boardnode/column-board-node.entity.ts | 3 +++ .../testing/factory/boardnode/column-board-node.factory.ts | 3 ++- .../factory/domainobject/board/column-board.do.factory.ts | 3 ++- 14 files changed, 32 insertions(+), 7 deletions(-) diff --git a/apps/server/src/modules/board/repo/board-do.builder-impl.ts b/apps/server/src/modules/board/repo/board-do.builder-impl.ts index 79b1ce51851..332a5d6e15e 100644 --- a/apps/server/src/modules/board/repo/board-do.builder-impl.ts +++ b/apps/server/src/modules/board/repo/board-do.builder-impl.ts @@ -1,6 +1,7 @@ import { NotImplementedException } from '@nestjs/common'; import { AnyBoardDo, + BoardLayout, Card, Column, ColumnBoard, @@ -61,6 +62,7 @@ export class BoardDoBuilderImpl implements BoardDoBuilder { updatedAt: boardNode.updatedAt, context: boardNode.context, isVisible: boardNode.isVisible ?? false, + layout: boardNode.layout ?? BoardLayout.COLUMNS, }); return columnBoard; diff --git a/apps/server/src/modules/board/repo/recursive-save.visitor.ts b/apps/server/src/modules/board/repo/recursive-save.visitor.ts index 9516809a748..51506bcadcd 100644 --- a/apps/server/src/modules/board/repo/recursive-save.visitor.ts +++ b/apps/server/src/modules/board/repo/recursive-save.visitor.ts @@ -72,6 +72,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { position: parentData?.position, context: columnBoard.context, isVisible: columnBoard.isVisible, + layout: columnBoard.layout, }); this.saveRecursive(boardNode, columnBoard); diff --git a/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts b/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts index 5895d2139e3..8398f59b5bb 100644 --- a/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts +++ b/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts @@ -57,6 +57,7 @@ export class RecursiveCopyVisitor implements BoardCompositeVisitorAsync { updatedAt: new Date(), children: this.getCopiesForChildrenOf(original), isVisible: false, + layout: original.layout ?? undefined, }); this.resultMap.set(original.id, { diff --git a/apps/server/src/modules/board/service/column-board.service.ts b/apps/server/src/modules/board/service/column-board.service.ts index 8cf84ed2cb7..c3f65350aa0 100644 --- a/apps/server/src/modules/board/service/column-board.service.ts +++ b/apps/server/src/modules/board/service/column-board.service.ts @@ -3,6 +3,7 @@ import { AnyBoardDo, BoardExternalReference, BoardExternalReferenceType, + BoardLayout, ColumnBoard, } from '@shared/domain/domainobject'; import { EntityId } from '@shared/domain/types'; @@ -37,7 +38,7 @@ export class ColumnBoardService { return titleMap; } - async create(context: BoardExternalReference, title = ''): Promise { + async create(context: BoardExternalReference, layout: BoardLayout, title = ''): Promise { const columnBoard = new ColumnBoard({ id: new ObjectId().toHexString(), title, @@ -46,6 +47,7 @@ export class ColumnBoardService { updatedAt: new Date(), context, isVisible: false, + layout, }); await this.boardDoRepo.save(columnBoard); diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index 363b7b3aa5d..76b7081257c 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -1,6 +1,6 @@ import { Action, AuthorizationService } from '@modules/authorization'; import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { BoardExternalReference, Column, ColumnBoard } from '@shared/domain/domainobject'; +import { BoardExternalReference, BoardLayout, Column, ColumnBoard } from '@shared/domain/domainobject'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; @@ -40,7 +40,9 @@ export class BoardUc extends BaseUc { }); const context = { type: params.parentType, id: params.parentId }; - const board = await this.columnBoardService.create(context, params.title); + + const boardLayout = params.layout ?? BoardLayout.COLUMNS; + const board = await this.columnBoardService.create(context, boardLayout, params.title); return board; } diff --git a/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts b/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts index 5471e4d40b4..d0a1f211574 100644 --- a/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts @@ -2,13 +2,14 @@ import { ApiProperty } from '@nestjs/swagger'; import { DecodeHtmlEntities } from '@shared/controller'; export class BoardColumnBoardResponse { - constructor({ id, columnBoardId, title, published, createdAt, updatedAt }: BoardColumnBoardResponse) { + constructor({ id, columnBoardId, title, published, createdAt, updatedAt, layout }: BoardColumnBoardResponse) { this.id = id; this.columnBoardId = columnBoardId; this.title = title; this.published = published; this.createdAt = createdAt; this.updatedAt = updatedAt; + this.layout = layout; } @ApiProperty() @@ -29,4 +30,7 @@ export class BoardColumnBoardResponse { @ApiProperty() columnBoardId: string; + + @ApiProperty() + layout: string; } diff --git a/apps/server/src/modules/learnroom/mapper/room-board-response.mapper.ts b/apps/server/src/modules/learnroom/mapper/room-board-response.mapper.ts index 66d10b74404..09703f57eec 100644 --- a/apps/server/src/modules/learnroom/mapper/room-board-response.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/room-board-response.mapper.ts @@ -99,6 +99,7 @@ export class RoomBoardResponseMapper { published: columnBoardInfo.published, createdAt: columnBoardInfo.createdAt, updatedAt: columnBoardInfo.updatedAt, + layout: columnBoardInfo.layout, }); const boardElementResponse = new BoardElementResponse({ diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts index 62b697332d7..12592fa4c8b 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { BoardExternalReferenceType, ColumnBoard } from '@shared/domain/domainobject'; +import { BoardExternalReferenceType, BoardLayout, ColumnBoard } from '@shared/domain/domainobject'; import { Course, User } from '@shared/domain/entity'; import { CardService, ColumnBoardService, ColumnService } from '@src/modules/board'; import { @@ -37,6 +37,7 @@ export class CommonCartridgeImportService { type: BoardExternalReferenceType.Course, id: course.id, }, + BoardLayout.COLUMNS, parser.manifest.getTitle() ); diff --git a/apps/server/src/modules/learnroom/types/room-board.types.ts b/apps/server/src/modules/learnroom/types/room-board.types.ts index 5c0297c6ff8..7f15863ec30 100644 --- a/apps/server/src/modules/learnroom/types/room-board.types.ts +++ b/apps/server/src/modules/learnroom/types/room-board.types.ts @@ -35,6 +35,7 @@ export type ColumnBoardMetaData = { published: boolean; createdAt: Date; updatedAt: Date; + layout: string; }; export type RoomBoardElementDTO = { diff --git a/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts b/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts index 2670bf81f84..49686ac757e 100644 --- a/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts +++ b/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts @@ -165,6 +165,7 @@ class DtoCreator { createdAt: columnBoardNode.createdAt, updatedAt: columnBoardNode.updatedAt, published: columnBoardNode.isVisible, + layout: columnBoardNode.layout || 'columns', }; return { type, content }; diff --git a/apps/server/src/shared/domain/domainobject/board/column-board.do.ts b/apps/server/src/shared/domain/domainobject/board/column-board.do.ts index a4e1dfe127e..d9f5bb9a784 100644 --- a/apps/server/src/shared/domain/domainobject/board/column-board.do.ts +++ b/apps/server/src/shared/domain/domainobject/board/column-board.do.ts @@ -32,6 +32,10 @@ export class ColumnBoard extends BoardComposite { return this.props.layout ?? BoardLayout.COLUMNS; } + /* set layout(layout: BoardLayout) { + this.props.layout = layout; + } */ + isAllowedAsChild(domainObject: AnyBoardDo): boolean { const allowed = domainObject instanceof Column; return allowed; diff --git a/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts index b757218e44c..1362d739054 100644 --- a/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts +++ b/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts @@ -22,6 +22,8 @@ export class ColumnBoardNode extends BoardNode implements LearnroomElement { this._contextId = new ObjectId(props.context.id); this.isVisible = props.isVisible ?? false; + + this.layout = props.layout ?? BoardLayout.COLUMNS; } @Property({ fieldName: 'contextType' }) @@ -65,4 +67,5 @@ export class ColumnBoardNode extends BoardNode implements LearnroomElement { export interface ColumnBoardNodeProps extends RootBoardNodeProps { isVisible: boolean; + layout: BoardLayout; } diff --git a/apps/server/src/shared/testing/factory/boardnode/column-board-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/column-board-node.factory.ts index 6fed4bbbe77..7b6377c2f2d 100644 --- a/apps/server/src/shared/testing/factory/boardnode/column-board-node.factory.ts +++ b/apps/server/src/shared/testing/factory/boardnode/column-board-node.factory.ts @@ -1,5 +1,5 @@ /* istanbul ignore file */ -import { BoardExternalReferenceType } from '@shared/domain/domainobject/board/types'; +import { BoardExternalReferenceType, BoardLayout } from '@shared/domain/domainobject/board/types'; import { ColumnBoardNode, ColumnBoardNodeProps } from '@shared/domain/entity'; import { ObjectId } from '@mikro-orm/mongodb'; import { BaseFactory } from '../base.factory'; @@ -14,6 +14,7 @@ export const columnBoardNodeFactory = BaseFactory.define Date: Mon, 15 Apr 2024 12:37:35 +0200 Subject: [PATCH 05/12] make layout property mandatory --- .../api-test/board-create.api.spec.ts | 125 ++++++++---------- .../dto/board/create-board.body.params.ts | 6 +- .../board/repo/board-do.builder-impl.ts | 5 +- .../recursive-copy.visitor.ts | 2 +- .../service/column-board.service.spec.ts | 5 +- .../board/service/column-board.service.ts | 4 +- apps/server/src/modules/board/uc/board.uc.ts | 5 +- .../mapper/room-board-response.mapper.spec.ts | 8 +- .../common-cartridge-import.service.ts | 2 +- .../domainobject/board/column-board.do.ts | 6 +- .../entity/boardnode/boardnode.entity.spec.ts | 3 +- .../boardnode/column-board-node.entity.ts | 2 +- 12 files changed, 75 insertions(+), 98 deletions(-) diff --git a/apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts index f747879f91f..433a00fef5c 100644 --- a/apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts @@ -4,7 +4,8 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { BoardExternalReferenceType, BoardLayout } from '@shared/domain/domainobject'; import { ColumnBoardNode } from '@shared/domain/entity'; -import { TestApiClient, UserAndAccountTestFactory, cleanupCollections, courseFactory } from '@shared/testing'; +import { cleanupCollections, courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { CreateBoardBodyParams } from '../dto'; const baseRouteName = '/boards'; @@ -50,10 +51,11 @@ describe(`create board (api)`, () => { const { loggedInClient, course } = await setup(); const title = 'new board'; - const response = await loggedInClient.post(undefined, { + const response = await loggedInClient.post(undefined, { title, parentId: course.id, parentType: BoardExternalReferenceType.Course, + layout: BoardLayout.COLUMNS, }); const boardId = (response.body as { id: string }).id; @@ -65,33 +67,12 @@ describe(`create board (api)`, () => { }); describe('Board layout', () => { - describe('When layout is omitted', () => { - it('should create a column board', async () => { - const { loggedInClient, course } = await setup(); - const title = 'new board'; - - const response = await loggedInClient.post(undefined, { - title, - parentId: course.id, - parentType: BoardExternalReferenceType.Course, - }); - - const boardId = (response.body as { id: string }).id; - expect(response.status).toEqual(201); - expect(boardId).toBeDefined(); - - const dbResult = await em.findOneOrFail(ColumnBoardNode, boardId); - expect(dbResult.layout).toBeUndefined(); - }); - }); - describe(`When layout is set to "${BoardLayout.COLUMNS}"`, () => { it('should create a column board', async () => { const { loggedInClient, course } = await setup(); - const title = 'new board'; - const response = await loggedInClient.post(undefined, { - title, + const response = await loggedInClient.post(undefined, { + title: 'new board', parentId: course.id, parentType: BoardExternalReferenceType.Course, layout: BoardLayout.COLUMNS, @@ -102,17 +83,16 @@ describe(`create board (api)`, () => { expect(boardId).toBeDefined(); const dbResult = await em.findOneOrFail(ColumnBoardNode, boardId); - expect(dbResult.layout).toBeUndefined(); + expect(dbResult.layout).toEqual(BoardLayout.COLUMNS); }); }); describe(`When layout is set to "${BoardLayout.LIST}"`, () => { it('should create a list board', async () => { const { loggedInClient, course } = await setup(); - const title = 'new board'; - const response = await loggedInClient.post(undefined, { - title, + const response = await loggedInClient.post(undefined, { + title: 'new board', parentId: course.id, parentType: BoardExternalReferenceType.Course, layout: BoardLayout.LIST, @@ -127,6 +107,36 @@ describe(`create board (api)`, () => { }); }); }); + + describe('When layout is omitted', () => { + it('should return status 400', async () => { + const { loggedInClient, course } = await setup(); + + const response = await loggedInClient.post(undefined, >{ + title: 'new board', + parentId: course.id, + parentType: BoardExternalReferenceType.Course, + layout: undefined, + }); + + expect(response.status).toEqual(400); + }); + }); + + describe('When layout is invalid', () => { + it('should return status 400', async () => { + const { loggedInClient, course } = await setup(); + + const response = await loggedInClient.post(undefined, >{ + title: 'new board', + parentId: course.id, + parentType: BoardExternalReferenceType.Course, + layout: 'invalid', + }); + + expect(response.status).toEqual(400); + }); + }); }); describe('When user is teacher and has no course permission', () => { @@ -144,12 +154,12 @@ describe(`create board (api)`, () => { it('should return status 403', async () => { const { loggedInClient, course } = await setup(); - const title = 'new board'; - const response = await loggedInClient.post(undefined, { - title, + const response = await loggedInClient.post(undefined, { + title: 'new board', parentId: course.id, parentType: BoardExternalReferenceType.Course, + layout: BoardLayout.COLUMNS, }); expect(response.status).toEqual(403); @@ -171,12 +181,12 @@ describe(`create board (api)`, () => { it('should return status 403', async () => { const { loggedInClient, course } = await setup(); - const title = 'new board'; - const response = await loggedInClient.post(undefined, { - title, + const response = await loggedInClient.post(undefined, { + title: 'new board', parentId: course.id, parentType: BoardExternalReferenceType.Course, + layout: BoardLayout.COLUMNS, }); expect(response.status).toEqual(403); @@ -200,10 +210,9 @@ describe(`create board (api)`, () => { it('should return status 400', async () => { const { loggedInClient, course } = await setup(); - const title = ''; - const response = await loggedInClient.post(undefined, { - title, + const response = await loggedInClient.post(undefined, { + title: '', parentId: course.id, parentType: BoardExternalReferenceType.Course, }); @@ -227,10 +236,9 @@ describe(`create board (api)`, () => { it('should return status 400', async () => { const { loggedInClient, course } = await setup(); - const title = 'a'.repeat(101); - const response = await loggedInClient.post(undefined, { - title, + const response = await loggedInClient.post(undefined, { + title: 'a'.repeat(101), parentId: course.id, parentType: BoardExternalReferenceType.Course, }); @@ -255,7 +263,7 @@ describe(`create board (api)`, () => { const { loggedInClient } = await setup(); const title = 'new board'; - const response = await loggedInClient.post(undefined, { + const response = await loggedInClient.post(undefined, { title, parentId: '123', parentType: BoardExternalReferenceType.Course, @@ -280,39 +288,12 @@ describe(`create board (api)`, () => { it('should return status 400', async () => { const { loggedInClient, course } = await setup(); - const title = 'new board'; - const response = await loggedInClient.post(undefined, { - title, + const response = await loggedInClient.post(undefined, >{ + title: 'new board', parentId: course.id, parentType: 'invalid', - }); - - expect(response.status).toEqual(400); - }); - }); - - describe('When layout is invalid', () => { - const setup = async () => { - const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); - - const course = courseFactory.build({ teachers: [teacherUser] }); - await em.persistAndFlush([teacherUser, course, teacherAccount]); - em.clear(); - - const loggedInClient = await testApiClient.login(teacherAccount); - - return { loggedInClient, course }; - }; - it('should return status 400', async () => { - const { loggedInClient, course } = await setup(); - const title = 'new board'; - - const response = await loggedInClient.post(undefined, { - title, - parentId: course.id, - parentType: BoardExternalReferenceType.Course, - layout: 'invalid', + layout: BoardLayout.COLUMNS, }); expect(response.status).toEqual(400); diff --git a/apps/server/src/modules/board/controller/dto/board/create-board.body.params.ts b/apps/server/src/modules/board/controller/dto/board/create-board.body.params.ts index 495b2d74a70..aa9f01924d5 100644 --- a/apps/server/src/modules/board/controller/dto/board/create-board.body.params.ts +++ b/apps/server/src/modules/board/controller/dto/board/create-board.body.params.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { SanitizeHtml } from '@shared/controller'; import { BoardExternalReferenceType, BoardLayout } from '@shared/domain/domainobject'; -import { IsEnum, IsMongoId, MaxLength, MinLength, ValidateIf } from 'class-validator'; +import { IsEnum, IsMongoId, MaxLength, MinLength } from 'class-validator'; export class CreateBoardBodyParams { @ApiProperty({ @@ -31,12 +31,10 @@ export class CreateBoardBodyParams { @ApiProperty({ description: 'The layout of the board', - required: false, default: BoardLayout.COLUMNS, enum: BoardLayout, enumName: 'BoardLayout', }) - @ValidateIf((o: CreateBoardBodyParams) => o.layout !== undefined) @IsEnum(BoardLayout, {}) - layout?: BoardLayout; + layout!: BoardLayout; } diff --git a/apps/server/src/modules/board/repo/board-do.builder-impl.ts b/apps/server/src/modules/board/repo/board-do.builder-impl.ts index 332a5d6e15e..82394678d82 100644 --- a/apps/server/src/modules/board/repo/board-do.builder-impl.ts +++ b/apps/server/src/modules/board/repo/board-do.builder-impl.ts @@ -1,7 +1,6 @@ import { NotImplementedException } from '@nestjs/common'; import { AnyBoardDo, - BoardLayout, Card, Column, ColumnBoard, @@ -17,9 +16,9 @@ import { SubmissionItem, } from '@shared/domain/domainobject'; import { + BoardNodeType, type BoardDoBuilder, type BoardNode, - BoardNodeType, type CardNode, type ColumnBoardNode, type ColumnNode, @@ -62,7 +61,7 @@ export class BoardDoBuilderImpl implements BoardDoBuilder { updatedAt: boardNode.updatedAt, context: boardNode.context, isVisible: boardNode.isVisible ?? false, - layout: boardNode.layout ?? BoardLayout.COLUMNS, + layout: boardNode.layout, }); return columnBoard; diff --git a/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts b/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts index 8398f59b5bb..240ec8fff1d 100644 --- a/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts +++ b/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts @@ -57,7 +57,7 @@ export class RecursiveCopyVisitor implements BoardCompositeVisitorAsync { updatedAt: new Date(), children: this.getCopiesForChildrenOf(original), isVisible: false, - layout: original.layout ?? undefined, + layout: original.layout, }); this.resultMap.set(original.id, { diff --git a/apps/server/src/modules/board/service/column-board.service.spec.ts b/apps/server/src/modules/board/service/column-board.service.spec.ts index d5609285f87..353f053967a 100644 --- a/apps/server/src/modules/board/service/column-board.service.spec.ts +++ b/apps/server/src/modules/board/service/column-board.service.spec.ts @@ -1,16 +1,17 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { IConfig } from '@hpi-schul-cloud/commons/lib/interfaces/IConfig'; +import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { BoardExternalReference, BoardExternalReferenceType, + BoardLayout, ColumnBoard, ContentElementFactory, } from '@shared/domain/domainobject'; import { columnBoardNodeFactory, setupEntities } from '@shared/testing'; import { columnBoardFactory, columnFactory, richTextElementFactory } from '@shared/testing/factory/domainobject'; -import { ObjectId } from '@mikro-orm/mongodb'; import { BoardDoRepo } from '../repo'; import { BoardDoService } from './board-do.service'; import { ColumnBoardService } from './column-board.service'; @@ -164,7 +165,7 @@ describe(ColumnBoardService.name, () => { const { context } = setupBoards(); const title = `My brand new Mainboard`; - const columnBoardInfo = await service.create(context, title); + const columnBoardInfo = await service.create(context, BoardLayout.COLUMNS, title); expect(columnBoardInfo).toEqual(expect.objectContaining({ title })); }); diff --git a/apps/server/src/modules/board/service/column-board.service.ts b/apps/server/src/modules/board/service/column-board.service.ts index c3f65350aa0..318882321db 100644 --- a/apps/server/src/modules/board/service/column-board.service.ts +++ b/apps/server/src/modules/board/service/column-board.service.ts @@ -1,3 +1,4 @@ +import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { AnyBoardDo, @@ -7,7 +8,6 @@ import { ColumnBoard, } from '@shared/domain/domainobject'; import { EntityId } from '@shared/domain/types'; -import { ObjectId } from '@mikro-orm/mongodb'; import { BoardDoRepo } from '../repo'; import { BoardDoService } from './board-do.service'; @@ -38,7 +38,7 @@ export class ColumnBoardService { return titleMap; } - async create(context: BoardExternalReference, layout: BoardLayout, title = ''): Promise { + async create(context: BoardExternalReference, layout: BoardLayout, title: string): Promise { const columnBoard = new ColumnBoard({ id: new ObjectId().toHexString(), title, diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index 76b7081257c..19b742c8f60 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -1,6 +1,6 @@ import { Action, AuthorizationService } from '@modules/authorization'; import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { BoardExternalReference, BoardLayout, Column, ColumnBoard } from '@shared/domain/domainobject'; +import { BoardExternalReference, Column, ColumnBoard } from '@shared/domain/domainobject'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; @@ -41,8 +41,7 @@ export class BoardUc extends BaseUc { const context = { type: params.parentType, id: params.parentId }; - const boardLayout = params.layout ?? BoardLayout.COLUMNS; - const board = await this.columnBoardService.create(context, boardLayout, params.title); + const board = await this.columnBoardService.create(context, params.layout, params.title); return board; } diff --git a/apps/server/src/modules/learnroom/mapper/room-board-response.mapper.spec.ts b/apps/server/src/modules/learnroom/mapper/room-board-response.mapper.spec.ts index 9244b453a21..36a5c9073c8 100644 --- a/apps/server/src/modules/learnroom/mapper/room-board-response.mapper.spec.ts +++ b/apps/server/src/modules/learnroom/mapper/room-board-response.mapper.spec.ts @@ -1,8 +1,9 @@ +import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; +import { BoardLayout } from '@shared/domain/domainobject'; import { courseFactory, setupEntities, taskFactory } from '@shared/testing'; -import { ObjectId } from '@mikro-orm/mongodb'; import { BoardElementResponse, SingleColumnBoardResponse } from '../controller/dto'; -import { RoomBoardDTO, RoomBoardElementTypes } from '../types'; +import { ColumnBoardMetaData, RoomBoardDTO, RoomBoardElementTypes } from '../types'; import { RoomBoardResponseMapper } from './room-board-response.mapper'; describe('room board response mapper', () => { @@ -157,11 +158,12 @@ describe('room board response mapper', () => { }); it('should map column board targets on board to response', () => { - const columnBoardMetaData = { + const columnBoardMetaData: ColumnBoardMetaData = { id: new ObjectId().toHexString(), columnBoardId: new ObjectId().toHexString(), title: 'column board #1', published: true, + layout: BoardLayout.COLUMNS, createdAt: new Date(), updatedAt: new Date(), }; diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts index 12592fa4c8b..0e0b9236a13 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts @@ -38,7 +38,7 @@ export class CommonCartridgeImportService { id: course.id, }, BoardLayout.COLUMNS, - parser.manifest.getTitle() + parser.manifest.getTitle() || '' ); await this.createColumns(parser, columnBoard); diff --git a/apps/server/src/shared/domain/domainobject/board/column-board.do.ts b/apps/server/src/shared/domain/domainobject/board/column-board.do.ts index d9f5bb9a784..7115e9a3c2c 100644 --- a/apps/server/src/shared/domain/domainobject/board/column-board.do.ts +++ b/apps/server/src/shared/domain/domainobject/board/column-board.do.ts @@ -29,13 +29,9 @@ export class ColumnBoard extends BoardComposite { } get layout(): BoardLayout { - return this.props.layout ?? BoardLayout.COLUMNS; + return this.props.layout; } - /* set layout(layout: BoardLayout) { - this.props.layout = layout; - } */ - isAllowedAsChild(domainObject: AnyBoardDo): boolean { const allowed = domainObject instanceof Column; return allowed; diff --git a/apps/server/src/shared/domain/entity/boardnode/boardnode.entity.spec.ts b/apps/server/src/shared/domain/entity/boardnode/boardnode.entity.spec.ts index 131789557ac..a8dd6d411ef 100644 --- a/apps/server/src/shared/domain/entity/boardnode/boardnode.entity.spec.ts +++ b/apps/server/src/shared/domain/entity/boardnode/boardnode.entity.spec.ts @@ -1,4 +1,4 @@ -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; +import { BoardExternalReferenceType, BoardLayout } from '@shared/domain/domainobject'; import { cardNodeFactory, columnBoardNodeFactory, setupEntities } from '@shared/testing'; import { BoardNode } from './boardnode.entity'; import { ColumnBoardNode } from './column-board-node.entity'; @@ -18,6 +18,7 @@ describe(BoardNode.name, () => { title: 'column #1', context: { type: BoardExternalReferenceType.Course, id: 'course1' }, isVisible: true, + layout: BoardLayout.COLUMNS, }); column.title = 'hate to get useless sonar lint errors'; }).toThrowError(); diff --git a/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts index 1362d739054..6c915b02dbb 100644 --- a/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts +++ b/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts @@ -43,7 +43,7 @@ export class ColumnBoardNode extends BoardNode implements LearnroomElement { } @Property({ nullable: true }) - layout?: BoardLayout; + layout: BoardLayout; useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { const domainObject = builder.buildColumnBoard(this); From 0a1340b3e84428a7c986d554b4f1b5a48c315e67 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Mon, 15 Apr 2024 13:07:27 +0200 Subject: [PATCH 06/12] implement migration to add layout field --- .../mikro-orm/Migration20240415124640.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 apps/server/src/migrations/mikro-orm/Migration20240415124640.ts diff --git a/apps/server/src/migrations/mikro-orm/Migration20240415124640.ts b/apps/server/src/migrations/mikro-orm/Migration20240415124640.ts new file mode 100644 index 00000000000..8cb380e903f --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20240415124640.ts @@ -0,0 +1,19 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; +import { BoardLayout } from '@shared/domain/domainobject'; +import { BoardNodeType } from '@shared/domain/entity'; + +export class Migration20240415124640 extends Migration { + async up(): Promise { + const columBoardResponse = await this.driver.nativeUpdate<{ type: BoardNodeType; layout: BoardLayout }>( + 'boardnodes', + { $and: [{ type: 'column-board' }, { layout: { $exists: false } }] }, + { layout: BoardLayout.COLUMNS } + ); + console.info(`Updated ${columBoardResponse.affectedRows} records in boardnodes`); + } + + // eslint-disable-next-line @typescript-eslint/require-await + async down(): Promise { + console.error(`boardnodes cannot be rolled-back. It must be restored from backup!`); + } +} From ed612bcd07b5b9dd7eeaa25642a2d26060959f33 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Mon, 15 Apr 2024 17:10:59 +0200 Subject: [PATCH 07/12] add feature flag --- apps/server/src/modules/server/api/dto/config.response.ts | 4 ++++ apps/server/src/modules/server/api/test/server.api.spec.ts | 1 + apps/server/src/modules/server/server.config.ts | 2 ++ config/default.schema.json | 6 +++++- config/development.json | 1 + 5 files changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/server/api/dto/config.response.ts b/apps/server/src/modules/server/api/dto/config.response.ts index 2c7e0df485f..a8c0fd43f20 100644 --- a/apps/server/src/modules/server/api/dto/config.response.ts +++ b/apps/server/src/modules/server/api/dto/config.response.ts @@ -125,6 +125,9 @@ export class ConfigResponse { @ApiProperty() FEATURE_TASK_SHARE: boolean; + @ApiProperty() + FEATURE_BOARD_LAYOUT_ENABLED: boolean; + @ApiProperty() FEATURE_USER_MIGRATION_ENABLED: boolean; @@ -225,6 +228,7 @@ export class ConfigResponse { this.FEATURE_LOGIN_LINK_ENABLED = config.FEATURE_LOGIN_LINK_ENABLED; this.FEATURE_LESSON_SHARE = config.FEATURE_LESSON_SHARE; this.FEATURE_TASK_SHARE = config.FEATURE_TASK_SHARE; + this.FEATURE_BOARD_LAYOUT_ENABLED = config.FEATURE_BOARD_LAYOUT_ENABLED; this.FEATURE_USER_MIGRATION_ENABLED = config.userMigrationEnabled; this.FEATURE_COPY_SERVICE_ENABLED = config.FEATURE_COPY_SERVICE_ENABLED; this.FEATURE_CONSENT_NECESSARY = config.FEATURE_CONSENT_NECESSARY; diff --git a/apps/server/src/modules/server/api/test/server.api.spec.ts b/apps/server/src/modules/server/api/test/server.api.spec.ts index 2785dbbd871..1a90b3be6fe 100644 --- a/apps/server/src/modules/server/api/test/server.api.spec.ts +++ b/apps/server/src/modules/server/api/test/server.api.spec.ts @@ -46,6 +46,7 @@ describe('Server Controller (API)', () => { 'FEATURE_COLUMN_BOARD_LINK_ELEMENT_ENABLED', 'FEATURE_COLUMN_BOARD_SUBMISSIONS_ENABLED', 'FEATURE_COLUMN_BOARD_SHARE', + 'FEATURE_BOARD_LAYOUT_ENABLED', 'FEATURE_CONSENT_NECESSARY', 'FEATURE_COPY_SERVICE_ENABLED', 'FEATURE_COURSE_SHARE', diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index 69cd3547b44..3251f1af0f7 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -76,6 +76,7 @@ export interface ServerConfig FEATURE_COLUMN_BOARD_LINK_ELEMENT_ENABLED: boolean; FEATURE_COLUMN_BOARD_EXTERNAL_TOOLS_ENABLED: boolean; FEATURE_COLUMN_BOARD_SHARE: boolean; + FEATURE_BOARD_LAYOUT_ENABLED: boolean; FEATURE_LOGIN_LINK_ENABLED: boolean; FEATURE_CONSENT_NECESSARY: boolean; FEATURE_SCHOOL_SANIS_USER_MIGRATION_ENABLED: boolean; @@ -128,6 +129,7 @@ const config: ServerConfig = { FEATURE_COURSE_SHARE: Configuration.get('FEATURE_COURSE_SHARE') as boolean, FEATURE_LESSON_SHARE: Configuration.get('FEATURE_LESSON_SHARE') as boolean, FEATURE_TASK_SHARE: Configuration.get('FEATURE_TASK_SHARE') as boolean, + FEATURE_BOARD_LAYOUT_ENABLED: Configuration.get('FEATURE_BOARD_LAYOUT_ENABLED') as boolean, FEATURE_LOGIN_LINK_ENABLED: Configuration.get('FEATURE_LOGIN_LINK_ENABLED') as boolean, FEATURE_COPY_SERVICE_ENABLED: Configuration.get('FEATURE_COPY_SERVICE_ENABLED') as boolean, FEATURE_CONSENT_NECESSARY: Configuration.get('FEATURE_CONSENT_NECESSARY') as boolean, diff --git a/config/default.schema.json b/config/default.schema.json index 5b6177ad980..1895032f1bb 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1155,6 +1155,11 @@ "default": false, "description": "Toggle for the column board sharing feature." }, + "FEATURE_BOARD_LAYOUT_ENABLED": { + "type": "boolean", + "default": false, + "description": "Toggle for the column board layout feature." + }, "FEATURE_USER_MIGRATION_ENABLED": { "type": "boolean", "default": false, @@ -1354,7 +1359,6 @@ "default": 60000, "description": "threshold in milliseconds to get deletionRequest to delete" } - }, "default": {} }, diff --git a/config/development.json b/config/development.json index b5478d63f10..dd82d4371e5 100644 --- a/config/development.json +++ b/config/development.json @@ -81,6 +81,7 @@ "FEATURE_COLUMN_BOARD_SUBMISSIONS_ENABLED": true, "FEATURE_COLUMN_BOARD_LINK_ELEMENT_ENABLED": true, "FEATURE_COLUMN_BOARD_EXTERNAL_TOOLS_ENABLED": true, + "FEATURE_BOARD_LAYOUT_ENABLED": true, "SCHULCONNEX_CLIENT": { "API_URL": "http://localhost:8888/v1/", "TOKEN_ENDPOINT": "http://localhost:8888/realms/SANIS/protocol/openid-connect/token", From bbbf1c1e31b8cfbccae0b77e4a78f945133fe0c8 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Tue, 16 Apr 2024 07:46:43 +0200 Subject: [PATCH 08/12] minor change --- apps/server/src/modules/board/poc/example.ts | 114 +++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 apps/server/src/modules/board/poc/example.ts diff --git a/apps/server/src/modules/board/poc/example.ts b/apps/server/src/modules/board/poc/example.ts new file mode 100644 index 00000000000..18f803360a4 --- /dev/null +++ b/apps/server/src/modules/board/poc/example.ts @@ -0,0 +1,114 @@ +enum BoardNodeType { + CARD = 'card', + COLUMN = 'column', +} + +interface BoardNodeProps { + id: string; + position: number; + type: BoardNodeType; +} + +interface CardProps extends BoardNodeProps { + height: number; + type: BoardNodeType.CARD; +} + +interface ColumnProps extends BoardNodeProps { + title: string; + type: BoardNodeType.COLUMN; +} + +// interface AllBoardNodeProps extends CardProps, ColumnProps {} +type AnyBoardNodeProps = CardProps | ColumnProps; + +class BoardNodeEntity implements Omit, Omit { + id!: string; + + position!: number; + + height!: number; + + title!: string; + + type!: BoardNodeType.CARD | BoardNodeType.COLUMN; +} + +abstract class BoardNode { + constructor(readonly props: T) {} + + abstract toString(): string; +} + +class Card extends BoardNode { + toString() { + return `card with height: ${this.props.height}`; + } +} + +class Column extends BoardNode { + toString() { + return `column with title: ${this.props.title}`; + } +} + +// type AnyBoardNode = Card | Column; + +// const props = new BoardNodeEntity(); +// props.height = 42; +// props.title = "Column #1"; +// props.type = BoardNodeType.COLUMN; + +// const node = createBoardNode(props); +// console.log(node.toString()); + +const cardProps: AnyBoardNodeProps = { + id: '42', + type: BoardNodeType.CARD, + position: 0, + height: 200, +}; + +const card = new Card(cardProps); +console.log(card.toString()); + +const columnProps: AnyBoardNodeProps = { + id: '44', + type: BoardNodeType.COLUMN, + position: 10, + title: 'Column #2', +}; + +const column = new Column(columnProps); +console.log(column.toString()); + +// from database +const props = new BoardNodeEntity(); +props.id = '42'; +props.type = BoardNodeType.CARD; +props.position = 0; +props.height = 422; + +const createNode = (entity: AnyBoardNodeProps) => { + if (props.type === BoardNodeType.CARD) { + return new Card(entity as CardProps); + } + if (props.type === BoardNodeType.COLUMN) { + return new Column(entity as ColumnProps); + } + throw new Error(); +}; + +const node = createNode(props); +console.log(node.toString()); + +// ---- +// https://stackoverflow.com/questions/36871057/does-typescript-support-subset-types + +// type Subset = U; + +// ---- + +// boardNode.removeChild(child); +// repo.deleteRecursive(child); +// repo.flush(); From 65a2877f45f8d6dd35df756d8e455bb76f41f128 Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Tue, 16 Apr 2024 07:50:06 +0200 Subject: [PATCH 09/12] Revert "minor change" This reverts commit bbbf1c1e31b8cfbccae0b77e4a78f945133fe0c8. --- apps/server/src/modules/board/poc/example.ts | 114 ------------------- 1 file changed, 114 deletions(-) delete mode 100644 apps/server/src/modules/board/poc/example.ts diff --git a/apps/server/src/modules/board/poc/example.ts b/apps/server/src/modules/board/poc/example.ts deleted file mode 100644 index 18f803360a4..00000000000 --- a/apps/server/src/modules/board/poc/example.ts +++ /dev/null @@ -1,114 +0,0 @@ -enum BoardNodeType { - CARD = 'card', - COLUMN = 'column', -} - -interface BoardNodeProps { - id: string; - position: number; - type: BoardNodeType; -} - -interface CardProps extends BoardNodeProps { - height: number; - type: BoardNodeType.CARD; -} - -interface ColumnProps extends BoardNodeProps { - title: string; - type: BoardNodeType.COLUMN; -} - -// interface AllBoardNodeProps extends CardProps, ColumnProps {} -type AnyBoardNodeProps = CardProps | ColumnProps; - -class BoardNodeEntity implements Omit, Omit { - id!: string; - - position!: number; - - height!: number; - - title!: string; - - type!: BoardNodeType.CARD | BoardNodeType.COLUMN; -} - -abstract class BoardNode { - constructor(readonly props: T) {} - - abstract toString(): string; -} - -class Card extends BoardNode { - toString() { - return `card with height: ${this.props.height}`; - } -} - -class Column extends BoardNode { - toString() { - return `column with title: ${this.props.title}`; - } -} - -// type AnyBoardNode = Card | Column; - -// const props = new BoardNodeEntity(); -// props.height = 42; -// props.title = "Column #1"; -// props.type = BoardNodeType.COLUMN; - -// const node = createBoardNode(props); -// console.log(node.toString()); - -const cardProps: AnyBoardNodeProps = { - id: '42', - type: BoardNodeType.CARD, - position: 0, - height: 200, -}; - -const card = new Card(cardProps); -console.log(card.toString()); - -const columnProps: AnyBoardNodeProps = { - id: '44', - type: BoardNodeType.COLUMN, - position: 10, - title: 'Column #2', -}; - -const column = new Column(columnProps); -console.log(column.toString()); - -// from database -const props = new BoardNodeEntity(); -props.id = '42'; -props.type = BoardNodeType.CARD; -props.position = 0; -props.height = 422; - -const createNode = (entity: AnyBoardNodeProps) => { - if (props.type === BoardNodeType.CARD) { - return new Card(entity as CardProps); - } - if (props.type === BoardNodeType.COLUMN) { - return new Column(entity as ColumnProps); - } - throw new Error(); -}; - -const node = createNode(props); -console.log(node.toString()); - -// ---- -// https://stackoverflow.com/questions/36871057/does-typescript-support-subset-types - -// type Subset = U; - -// ---- - -// boardNode.removeChild(child); -// repo.deleteRecursive(child); -// repo.flush(); From 31671017e83a015baa1d31a6cd9aa014d20cd33b Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Tue, 16 Apr 2024 07:51:03 +0200 Subject: [PATCH 10/12] minor change --- .../modules/board/controller/api-test/board-lookup.api.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts index 37399ca4aa6..460242684bf 100644 --- a/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts @@ -73,7 +73,7 @@ describe(`board lookup (api)`, () => { expect(response.status).toEqual(200); }); - it('should return the right board', async () => { + it('should return the correct board', async () => { const { loggedInClient, columnBoardNode, columnNode } = await setup(); const response = await loggedInClient.get(columnBoardNode.id); From cbe4667b82fd06b91c0ecba7f8e179a01bfac6cc Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Thu, 18 Apr 2024 11:44:22 +0200 Subject: [PATCH 11/12] update layout type of learntoom api response --- .../dto/single-column-board/board-column-board.response.ts | 3 ++- apps/server/src/modules/learnroom/types/room-board.types.ts | 3 ++- apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts | 2 +- .../shared/domain/entity/boardnode/column-board-node.entity.ts | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts b/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts index d0a1f211574..9975470646d 100644 --- a/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { DecodeHtmlEntities } from '@shared/controller'; +import { BoardLayout } from '@shared/domain/domainobject'; export class BoardColumnBoardResponse { constructor({ id, columnBoardId, title, published, createdAt, updatedAt, layout }: BoardColumnBoardResponse) { @@ -32,5 +33,5 @@ export class BoardColumnBoardResponse { columnBoardId: string; @ApiProperty() - layout: string; + layout: BoardLayout; } diff --git a/apps/server/src/modules/learnroom/types/room-board.types.ts b/apps/server/src/modules/learnroom/types/room-board.types.ts index 7f15863ec30..d657e0d1a9b 100644 --- a/apps/server/src/modules/learnroom/types/room-board.types.ts +++ b/apps/server/src/modules/learnroom/types/room-board.types.ts @@ -1,3 +1,4 @@ +import { BoardLayout } from '@shared/domain/domainobject'; import { TaskWithStatusVo } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; @@ -35,7 +36,7 @@ export type ColumnBoardMetaData = { published: boolean; createdAt: Date; updatedAt: Date; - layout: string; + layout: BoardLayout; }; export type RoomBoardElementDTO = { diff --git a/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts b/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts index 49686ac757e..5ea348cb088 100644 --- a/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts +++ b/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts @@ -165,7 +165,7 @@ class DtoCreator { createdAt: columnBoardNode.createdAt, updatedAt: columnBoardNode.updatedAt, published: columnBoardNode.isVisible, - layout: columnBoardNode.layout || 'columns', + layout: columnBoardNode.layout, }; return { type, content }; diff --git a/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts index 6c915b02dbb..237ae44bd61 100644 --- a/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts +++ b/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts @@ -42,7 +42,7 @@ export class ColumnBoardNode extends BoardNode implements LearnroomElement { }; } - @Property({ nullable: true }) + @Property({ nullable: false }) layout: BoardLayout; useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { From 1e94473823e3c44bc916d696c0773a3d35406795 Mon Sep 17 00:00:00 2001 From: virgilchiriac Date: Fri, 19 Apr 2024 10:34:16 +0200 Subject: [PATCH 12/12] BC-6770 - add migration to seed data --- backup/setup/migrations.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index fa332adf663..515b060f23a 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -61,5 +61,14 @@ "created_at": { "$date": "2024-03-26T14:42:51.024Z" } + }, + { + "_id": { + "$oid": "66222c3267551d7ebd81c096" + }, + "name": "Migration20240415124640", + "created_at": { + "$date": "2024-04-19T08:32:50.668Z" + } } ]