Skip to content

Commit

Permalink
N21-2031 Placeholder element for deleted tools in boards (#5182)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarvinOehlerkingCap authored Aug 22, 2024
1 parent 7a90ad7 commit 3ca9d3d
Show file tree
Hide file tree
Showing 57 changed files with 1,050 additions and 210 deletions.
2 changes: 2 additions & 0 deletions apps/server/src/modules/board/board.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
BoardNodeAuthorizableService,
BoardNodeService,
ColumnBoardService,
ContextExternalToolDeletedEventHandlerService,
MediaBoardService,
UserDeletedEventHandlerService,
} from './service';
Expand Down Expand Up @@ -60,6 +61,7 @@ import {
ColumnBoardReferenceService,
ColumnBoardTitleService,
UserDeletedEventHandlerService,
ContextExternalToolDeletedEventHandlerService,
// TODO replace by import of MediaBoardModule (fix dependency cycle)
MediaBoardService,
],
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/modules/board/controller/card.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
CardListResponse,
CardUrlParams,
CreateContentElementBodyParams,
DeletedElementResponse,
DrawingElementResponse,
ExternalToolElementResponse,
FileElementResponse,
Expand Down Expand Up @@ -121,7 +122,9 @@ export class CardController {
FileElementResponse,
LinkElementResponse,
RichTextElementResponse,
SubmissionContainerElementResponse
SubmissionContainerElementResponse,
DrawingElementResponse,
DeletedElementResponse
)
@ApiResponse({
status: 201,
Expand All @@ -133,6 +136,7 @@ export class CardController {
{ $ref: getSchemaPath(RichTextElementResponse) },
{ $ref: getSchemaPath(SubmissionContainerElementResponse) },
{ $ref: getSchemaPath(DrawingElementResponse) },
{ $ref: getSchemaPath(DeletedElementResponse) },
],
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DecodeHtmlEntities } from '@shared/controller';
import {
AnyContentElementResponse,
CollaborativeTextEditorElementResponse,
DeletedElementResponse,
DrawingElementResponse,
ExternalToolElementResponse,
FileElementResponse,
Expand All @@ -20,7 +21,8 @@ import { VisibilitySettingsResponse } from './visibility-settings.response';
RichTextElementResponse,
DrawingElementResponse,
SubmissionContainerElementResponse,
CollaborativeTextEditorElementResponse
CollaborativeTextEditorElementResponse,
DeletedElementResponse
)
export class CardResponse {
constructor({ id, title, height, elements, visibilitySettings, timestamps }: CardResponse) {
Expand Down Expand Up @@ -55,6 +57,7 @@ export class CardResponse {
{ $ref: getSchemaPath(SubmissionContainerElementResponse) },
{ $ref: getSchemaPath(DrawingElementResponse) },
{ $ref: getSchemaPath(CollaborativeTextEditorElementResponse) },
{ $ref: getSchemaPath(DeletedElementResponse) },
],
},
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CollaborativeTextEditorElementResponse } from './collaborative-text-editor-element.response';
import { DeletedElementResponse } from './deleted-element.response';
import { DrawingElementResponse } from './drawing-element.response';
import { ExternalToolElementResponse } from './external-tool-element.response';
import { FileElementResponse } from './file-element.response';
Expand All @@ -13,7 +14,8 @@ export type AnyContentElementResponse =
| SubmissionContainerElementResponse
| ExternalToolElementResponse
| DrawingElementResponse
| CollaborativeTextEditorElementResponse;
| CollaborativeTextEditorElementResponse
| DeletedElementResponse;

export const isFileElementResponse = (element: AnyContentElementResponse): element is FileElementResponse =>
element instanceof FileElementResponse;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ApiProperty } from '@nestjs/swagger';
import { ContentElementType } from '../../../domain';
import { TimestampsResponse } from '../timestamps.response';

export class DeletedElementContent {
constructor(props: DeletedElementContent) {
this.title = props.title;
this.deletedElementType = props.deletedElementType;
}

@ApiProperty()
title: string;

@ApiProperty({ enum: ContentElementType, enumName: 'ContentElementType' })
deletedElementType: ContentElementType;
}

export class DeletedElementResponse {
constructor(props: DeletedElementResponse) {
this.id = props.id;
this.type = props.type;
this.content = props.content;
this.timestamps = props.timestamps;
}

@ApiProperty({ pattern: '[a-f0-9]{24}' })
id: string;

@ApiProperty({ enum: ContentElementType, enumName: 'ContentElementType' })
type: ContentElementType.DELETED;

@ApiProperty()
content: DeletedElementContent;

@ApiProperty()
timestamps: TimestampsResponse;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './link-element.response';
export * from './rich-text-element.response';
export * from './submission-container-element.response';
export * from './update-element-content.body.params';
export * from './deleted-element.response';
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { NotImplementedException } from '@nestjs/common';
import {
fileElementFactory,
deletedElementFactory,
drawingElementFactory,
fileElementFactory,
linkElementFactory,
richTextElementFactory,
submissionContainerElementFactory,
} from '../../testing';
import {
DeletedElementResponse,
DrawingElementResponse,
FileElementResponse,
LinkElementResponse,
DrawingElementResponse,
RichTextElementResponse,
SubmissionContainerElementResponse,
} from '../dto';
Expand Down Expand Up @@ -55,6 +57,14 @@ describe(ContentElementResponseFactory.name, () => {
expect(result).toBeInstanceOf(SubmissionContainerElementResponse);
});

it('should return instance of DeletedElementResponse', () => {
const drawingElement = deletedElementFactory.build();

const result = ContentElementResponseFactory.mapToResponse(drawingElement);

expect(result).toBeInstanceOf(DeletedElementResponse);
});

it('should throw NotImplementedException', () => {
// @ts-expect-error check unknown type
expect(() => ContentElementResponseFactory.mapToResponse('UNKNOWN')).toThrow(NotImplementedException);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { AnyBoardNode, FileElement, RichTextElement } from '../../domain';
import {
AnyContentElementResponse,
FileElementResponse,
RichTextElementResponse,
isFileElementResponse,
isRichTextElementResponse,
RichTextElementResponse,
} from '../dto';
import { BaseResponseMapper } from './base-mapper.interface';
import { CollaborativeTextEditorElementResponseMapper } from './collaborative-text-editor-element-response.mapper';
import { DeletedElementResponseMapper } from './deleted-element-response.mapper';
import { DrawingElementResponseMapper } from './drawing-element-response.mapper';
import { ExternalToolElementResponseMapper } from './external-tool-element-response.mapper';
import { FileElementResponseMapper } from './file-element-response.mapper';
Expand All @@ -25,6 +26,7 @@ export class ContentElementResponseFactory {
SubmissionContainerElementResponseMapper.getInstance(),
ExternalToolElementResponseMapper.getInstance(),
CollaborativeTextEditorElementResponseMapper.getInstance(),
DeletedElementResponseMapper.getInstance(),
];

static mapToResponse(element: AnyBoardNode): AnyContentElementResponse {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ContentElementType, DeletedElement } from '../../domain';
import { DeletedElementContent, DeletedElementResponse, TimestampsResponse } from '../dto';
import { BaseResponseMapper } from './base-mapper.interface';

export class DeletedElementResponseMapper implements BaseResponseMapper {
private static instance: DeletedElementResponseMapper;

public static getInstance(): DeletedElementResponseMapper {
if (!DeletedElementResponseMapper.instance) {
DeletedElementResponseMapper.instance = new DeletedElementResponseMapper();
}

return DeletedElementResponseMapper.instance;
}

mapToResponse(element: DeletedElement): DeletedElementResponse {
const result = new DeletedElementResponse({
id: element.id,
timestamps: new TimestampsResponse({ lastUpdatedAt: element.updatedAt, createdAt: element.createdAt }),
type: ContentElementType.DELETED,
content: new DeletedElementContent({ title: element.title, deletedElementType: element.deletedElementType }),
});

return result;
}

canMap(element: unknown): boolean {
return element instanceof DeletedElement;
}
}
1 change: 1 addition & 0 deletions apps/server/src/modules/board/controller/mapper/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './link-element-response.mapper';
export * from './rich-text-element-response.mapper';
export * from './submission-container-element-response.mapper';
export * from './submission-item-response.mapper';
export * from './deleted-element-response.mapper';
4 changes: 4 additions & 0 deletions apps/server/src/modules/board/domain/board-node.do.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export abstract class BoardNode<T extends BoardNodeProps> extends DomainObject<T
return copyProps;
}

public getTrueProps(): T {
return this.props;
}

get level(): number {
return this.ancestorIds.length;
}
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/modules/board/domain/board-node.factory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ObjectId } from '@mikro-orm/mongodb';
import { Injectable, NotImplementedException } from '@nestjs/common';
import { Injectable, NotImplementedException, UnprocessableEntityException } from '@nestjs/common';
import { EntityId, InputFormat } from '@shared/domain/types';
import { Card } from './card.do';
import { CollaborativeTextEditorElement } from './collaborative-text-editor.do';
Expand Down Expand Up @@ -79,6 +79,8 @@ export class BoardNodeFactory {
...this.getBaseProps(),
});
break;
case ContentElementType.DELETED:
throw new UnprocessableEntityException('Deleted elements cannot be created from the outside');
case ContentElementType.COLLABORATIVE_TEXT_EDITOR:
element = new CollaborativeTextEditorElement({
...this.getBaseProps(),
Expand Down
53 changes: 53 additions & 0 deletions apps/server/src/modules/board/domain/deleted-element.do.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ObjectId } from '@mikro-orm/mongodb';
import { DeletedElement } from './deleted-element.do';
import { BoardNodeProps, ContentElementType } from './types';

describe(DeletedElement.name, () => {
let element: DeletedElement;

const boardNodeProps: BoardNodeProps = {
id: new ObjectId().toHexString(),
path: '',
level: 1,
position: 1,
children: [],
createdAt: new Date(),
updatedAt: new Date(),
};

beforeEach(() => {
element = new DeletedElement({
...boardNodeProps,
deletedElementType: ContentElementType.EXTERNAL_TOOL,
title: 'Old Tool',
});
});

it('should return title', () => {
expect(element.title).toEqual('Old Tool');
});

it('should set title', () => {
const title = 'Title';

element.title = title;

expect(element.title).toEqual(title);
});

it('should return deletedElementType', () => {
expect(element.deletedElementType).toEqual(ContentElementType.EXTERNAL_TOOL);
});

it('should set deletedElementType', () => {
const deletedElementType = ContentElementType.FILE;

element.deletedElementType = deletedElementType;

expect(element.deletedElementType).toEqual(deletedElementType);
});

it('should not have child', () => {
expect(element.canHaveChild()).toEqual(false);
});
});
27 changes: 27 additions & 0 deletions apps/server/src/modules/board/domain/deleted-element.do.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { BoardNode } from './board-node.do';
import type { ContentElementType, DeletedElementProps } from './types';

export class DeletedElement extends BoardNode<DeletedElementProps> {
get title(): string {
return this.props.title;
}

set title(value: string) {
this.props.title = value;
}

get deletedElementType(): ContentElementType {
return this.props.deletedElementType;
}

set deletedElementType(value: ContentElementType) {
this.props.deletedElementType = value;
}

canHaveChild(): boolean {
return false;
}
}

export const isDeletedElement = (reference: unknown): reference is DeletedElement =>
reference instanceof DeletedElement;
1 change: 1 addition & 0 deletions apps/server/src/modules/board/domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from './submission-item.do';
export * from './path-utils';
export * from './types';
export * from './type-mapping';
export * from './deleted-element.do';
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,3 @@ import type { MediaExternalToolElement } from '../media-external-tool-element.do
import type { MediaLine } from '../media-line.do';

export type AnyMediaBoardNode = MediaBoard | MediaLine | MediaExternalToolElement;

// TODO remove if not needed
// export type AnyMediaBoardNode = MediaExternalToolElement;
/*
export const isAnyMediaContentElement = (element: AnyMediaBoardNode): element is AnyMediaBoardNode => {
const result: boolean = element instanceof MediaExternalToolElement;
return result;
};
*/
2 changes: 2 additions & 0 deletions apps/server/src/modules/board/domain/type-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Card } from './card.do';
import { CollaborativeTextEditorElement } from './collaborative-text-editor.do';
import { ColumnBoard } from './colum-board.do';
import { Column } from './column.do';
import { DeletedElement } from './deleted-element.do';
import { DrawingElement } from './drawing-element.do';
import { ExternalToolElement } from './external-tool-element.do';
import { FileElement } from './file-element.do';
Expand Down Expand Up @@ -30,6 +31,7 @@ const BoardNodeTypeToConstructor = {
[BoardNodeType.RICH_TEXT_ELEMENT]: RichTextElement,
[BoardNodeType.SUBMISSION_CONTAINER_ELEMENT]: SubmissionContainerElement,
[BoardNodeType.SUBMISSION_ITEM]: SubmissionItem,
[BoardNodeType.DELETED_ELEMENT]: DeletedElement,
} as const;

export const getBoardNodeConstructor = <T extends BoardNodeType>(type: T): typeof BoardNodeTypeToConstructor[T] =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type CollaborativeTextEditorElement, isCollaborativeTextEditorElement } from '../collaborative-text-editor.do';
import { type DeletedElement, isDeletedElement } from '../deleted-element.do';
import { type DrawingElement, isDrawingElement } from '../drawing-element.do';
import { type ExternalToolElement, isExternalToolElement } from '../external-tool-element.do';
import { type FileElement, isFileElement } from '../file-element.do';
Expand All @@ -14,7 +15,8 @@ export type AnyContentElement =
| FileElement
| LinkElement
| RichTextElement
| SubmissionContainerElement;
| SubmissionContainerElement
| DeletedElement;

export const isContentElement = (boardNode: AnyBoardNode): boardNode is AnyContentElement => {
const result =
Expand All @@ -24,7 +26,8 @@ export const isContentElement = (boardNode: AnyBoardNode): boardNode is AnyConte
isFileElement(boardNode) ||
isLinkElement(boardNode) ||
isRichTextElement(boardNode) ||
isSubmissionContainerElement(boardNode);
isSubmissionContainerElement(boardNode) ||
isDeletedElement(boardNode);

return result;
};
Loading

0 comments on commit 3ca9d3d

Please sign in to comment.