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-2031 Placeholder element for deleted tools in boards #5182

Merged
merged 11 commits into from
Aug 22, 2024
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
Loading