Skip to content

Commit

Permalink
N21-2358 Link elements display card title for board card links (#5467)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarvinOehlerkingCap authored Jan 27, 2025
1 parent d2b2bf5 commit 879c6cd
Show file tree
Hide file tree
Showing 20 changed files with 363 additions and 106 deletions.
12 changes: 9 additions & 3 deletions apps/server/src/modules/board/service/column-board.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { CopyStatus } from '@modules/copy-helper';
import { Injectable } from '@nestjs/common';
import { EntityId } from '@shared/domain/types';
import { BoardExternalReference, BoardExternalReferenceType, ColumnBoard, isColumnBoard } from '../domain';
import {
AnyBoardNode,
BoardExternalReference,
BoardExternalReferenceType,
ColumnBoard,
isColumnBoard,
} from '../domain';
import { BoardNodeRepo } from '../repo';
import { BoardNodeService } from './board-node.service';
import { ColumnBoardCopyService, ColumnBoardLinkService, CopyColumnBoardParams } from './internal';
Expand All @@ -22,9 +28,9 @@ export class ColumnBoardService {
}

async findByExternalReference(reference: BoardExternalReference, depth?: number): Promise<ColumnBoard[]> {
const boardNodes = await this.boardNodeRepo.findByExternalReference(reference, depth);
const boardNodes: AnyBoardNode[] = await this.boardNodeRepo.findByExternalReference(reference, depth);

const boards = boardNodes.filter((bn) => isColumnBoard(bn));
const boards: ColumnBoard[] = boardNodes.filter((bn: AnyBoardNode): bn is ColumnBoard => isColumnBoard(bn));

return boards;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MetaDataEntityType } from '../../types';
import { MetaTagExtractorResponse } from './meta-tag-extractor.response';

describe(MetaTagExtractorResponse.name, () => {
Expand All @@ -8,9 +9,9 @@ describe(MetaTagExtractorResponse.name, () => {
title: 'Testbild',
description: 'Here we describe what this page is about.',
imageUrl: 'https://www.abc.de/test.png',
type: 'unknown',
type: MetaDataEntityType.UNKNOWN,
parentTitle: 'Math',
parentType: 'course',
parentType: MetaDataEntityType.COURSE,
};

const response = new MetaTagExtractorResponse(properties);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { DecodeHtmlEntities } from '@shared/controller/transformer';
import { IsString, IsUrl } from 'class-validator';
import { MetaDataEntityType } from '../../types';

export class MetaTagExtractorResponse {
Expand All @@ -25,34 +24,29 @@ export class MetaTagExtractorResponse {
}

@ApiProperty()
@IsUrl()
url!: string;
public url: string;

@ApiProperty()
@DecodeHtmlEntities()
title?: string;
public title: string;

@ApiProperty()
@DecodeHtmlEntities()
description?: string;
public description: string;

@ApiProperty()
@IsString()
originalImageUrl?: string;
@ApiPropertyOptional()
public originalImageUrl?: string;

@ApiProperty()
@IsString()
imageUrl?: string;
@ApiPropertyOptional()
public imageUrl?: string;

@ApiProperty()
@IsString()
type: MetaDataEntityType;
@ApiProperty({ enum: MetaDataEntityType, enumName: 'MetaDataEntityType' })
public type: MetaDataEntityType;

@ApiProperty()
@ApiPropertyOptional()
@DecodeHtmlEntities()
parentTitle?: string;
public parentTitle?: string;

@ApiProperty()
@IsString()
parentType?: MetaDataEntityType;
@ApiPropertyOptional({ enum: MetaDataEntityType, enumName: 'MetaDataEntityType' })
public parentType?: MetaDataEntityType;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export class MetaTagExtractorController {
@Body() bodyParams: GetMetaTagDataBody
): Promise<MetaTagExtractorResponse> {
const result = await this.metaTagExtractorUc.getMetaData(currentUser.userId, bodyParams.url);

const response = new MetaTagExtractorResponse({ ...result });

return response;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import axios from 'axios';
import ogs from 'open-graph-scraper';
import { ImageObject } from 'open-graph-scraper/types/lib/types';
import { InvalidLinkUrlLoggableException } from '../loggable/invalid-link-url.loggable';
import { MetaData } from '../types';
import { MetaData, MetaDataEntityType } from '../types';

@Injectable()
export class MetaTagExternalUrlService {
Expand All @@ -21,7 +21,7 @@ export class MetaTagExternalUrlService {
description: ogDescription ?? '',
originalImageUrl: this.getImageUrl(ogImage, url),
url: url.toString(),
type: 'external',
type: MetaDataEntityType.EXTERNAL,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { Configuration } from '@hpi-schul-cloud/commons/lib';
import { Test, TestingModule } from '@nestjs/testing';
import { setupEntities } from '@testing/setup-entities';
import { MetaDataEntityType } from '../types';
import { MetaTagExternalUrlService } from './meta-tag-external-url.service';
import { MetaTagExtractorService } from './meta-tag-extractor.service';
import { MetaTagInternalUrlService } from './meta-tag-internal-url.service';
Expand Down Expand Up @@ -77,7 +78,7 @@ describe(MetaTagExtractorService.name, () => {
url,
title: 'super.pdf',
description: '',
type: 'unknown',
type: MetaDataEntityType.UNKNOWN,
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import net from 'net';
import { basename } from 'path';
import { InvalidLinkUrlLoggableException } from '../loggable/invalid-link-url.loggable';
import type { MetaData } from '../types';
import { MetaDataEntityType } from '../types';
import { MetaTagExternalUrlService } from './meta-tag-external-url.service';
import { MetaTagInternalUrlService } from './meta-tag-internal-url.service';

Expand All @@ -13,7 +14,7 @@ export class MetaTagExtractorService {
private readonly externalLinkMetaTagService: MetaTagExternalUrlService
) {}

async getMetaData(urlString: string): Promise<MetaData> {
public async getMetaData(urlString: string): Promise<MetaData> {
const url = this.parseValidUrl(urlString);

const metaData =
Expand All @@ -24,7 +25,7 @@ export class MetaTagExtractorService {
return metaData;
}

parseValidUrl(url: string): URL {
private parseValidUrl(url: string): URL {
const urlObject = new URL(url);

// enforce https
Expand All @@ -36,11 +37,11 @@ export class MetaTagExtractorService {
return urlObject;
}

private async tryInternalLinkMetaTags(url: URL): Promise<MetaData | undefined> {
private tryInternalLinkMetaTags(url: URL): Promise<MetaData | undefined> {
return this.internalLinkMataTagService.tryInternalLinkMetaTags(url);
}

private async tryExtractMetaTagsFromExternalUrl(url: URL): Promise<MetaData | undefined> {
private tryExtractMetaTagsFromExternalUrl(url: URL): Promise<MetaData | undefined> {
return this.externalLinkMetaTagService.tryExtractMetaTags(url);
}

Expand All @@ -49,7 +50,7 @@ export class MetaTagExtractorService {
title: basename(url.pathname),
description: '',
url: url.toString(),
type: 'unknown',
type: MetaDataEntityType.UNKNOWN,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { Configuration } from '@hpi-schul-cloud/commons/lib';
import { Test, TestingModule } from '@nestjs/testing';
import { setupEntities } from '@testing/setup-entities';
import { MetaData } from '../types';
import { MetaData, MetaDataEntityType } from '../types';
import { MetaTagInternalUrlService } from './meta-tag-internal-url.service';
import { BoardUrlHandler, CourseUrlHandler, LessonUrlHandler, TaskUrlHandler } from './url-handler';

Expand Down Expand Up @@ -101,7 +101,7 @@ describe(MetaTagInternalUrlService.name, () => {
title: 'My Title',
url: INTERNAL_URL.toString(),
description: '',
type: 'course',
type: MetaDataEntityType.COURSE,
};

return { mockedMetaTags };
Expand All @@ -126,7 +126,7 @@ describe(MetaTagInternalUrlService.name, () => {

const result = await service.tryInternalLinkMetaTags(UNKNOWN_INTERNAL_URL);

expect(result).toEqual(expect.objectContaining({ type: 'unknown' }));
expect(result).toEqual(expect.objectContaining({ type: MetaDataEntityType.UNKNOWN }));
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Configuration } from '@hpi-schul-cloud/commons/lib';
import { Injectable } from '@nestjs/common';
import type { UrlHandler } from '../interface/url-handler';
import { MetaData } from '../types';
import { MetaData, MetaDataEntityType } from '../types';
import { BoardUrlHandler, CourseUrlHandler, LessonUrlHandler, TaskUrlHandler } from './url-handler';

@Injectable()
Expand All @@ -17,14 +17,14 @@ export class MetaTagInternalUrlService {
this.handlers = [this.taskUrlHandler, this.lessonUrlHandler, this.courseUrlHandler, this.boardUrlHandler];
}

async tryInternalLinkMetaTags(url: URL): Promise<MetaData | undefined> {
public tryInternalLinkMetaTags(url: URL): Promise<MetaData | undefined> {
if (this.isInternalUrl(url)) {
return this.composeMetaTags(url);
}
return Promise.resolve(undefined);
}

isInternalUrl(url: URL) {
public isInternalUrl(url: URL): boolean {
let domain = Configuration.get('SC_DOMAIN') as string;
domain = domain === '' ? 'nothing-configured-for-internal-url.de' : domain;
const isInternal = url.hostname.toLowerCase() === domain.toLowerCase();
Expand All @@ -33,16 +33,18 @@ export class MetaTagInternalUrlService {

private async composeMetaTags(url: URL): Promise<MetaData | undefined> {
const handler = this.handlers.find((h) => h.doesUrlMatch(url));

if (handler) {
const result = await handler.getMetaData(url);

return result;
}

return Promise.resolve({
title: url.pathname,
description: '',
url: url.toString(),
type: 'unknown',
type: MetaDataEntityType.UNKNOWN,
});
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { basename } from 'node:path';
import { MetaData, MetaDataEntityType } from '../../types';
import { AbstractUrlHandler } from './abstract-url-handler';

class DummyHandler extends AbstractUrlHandler {
Expand Down Expand Up @@ -48,12 +50,46 @@ describe(AbstractUrlHandler.name, () => {
});

describe('getDefaultMetaData', () => {
it('should return meta data of type unknown', () => {
const { url, handler } = setup();
describe('when required fields are undefined', () => {
it('should return meta data with defaults', () => {
const { url, handler } = setup();

const result = handler.getDefaultMetaData(url, {
type: undefined,
url: undefined,
title: undefined,
description: undefined,
});

const result = handler.getDefaultMetaData(url);
expect(result).toEqual<MetaData>({
type: MetaDataEntityType.UNKNOWN,
url: url.toString(),
title: basename(url.pathname),
description: '',
});
});
});

expect(result).toEqual(expect.objectContaining({ type: 'unknown', url: url.toString() }));
describe('when partial overwrites the defaults', () => {
it('should return meta data with overwrites', () => {
const { url, handler } = setup();

const result = handler.getDefaultMetaData(url, {
type: MetaDataEntityType.BOARD,
url: 'url',
title: 'title',
description: 'description',
originalImageUrl: 'originalImageUrl',
});

expect(result).toEqual<MetaData>({
type: MetaDataEntityType.BOARD,
url: 'url',
title: 'title',
description: 'description',
originalImageUrl: 'originalImageUrl',
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,35 +1,38 @@
import { basename } from 'node:path';
import { MetaData } from '../../types';
import { MetaData, MetaDataEntityType } from '../../types';

export abstract class AbstractUrlHandler {
protected abstract patterns: RegExp[];

protected extractId(url: URL): string | undefined {
const results: RegExpMatchArray = this.patterns
.map((pattern: RegExp) => pattern.exec(url.toString()))
.filter((result) => result !== null)
.find((result) => (result?.length ?? 0) >= 2) as RegExpMatchArray;
const results: RegExpExecArray | undefined = this.patterns
.map((pattern: RegExp) => pattern.exec(url.pathname))
.filter((result: RegExpExecArray | null): result is RegExpExecArray => result !== null)
.find((result: RegExpExecArray) => result.length >= 2);

if (results && results[1]) {
return results[1];
}

return undefined;
}

doesUrlMatch(url: URL): boolean {
const doesMatch = this.patterns.some((pattern) => pattern.test(url.toString()));
public doesUrlMatch(url: URL): boolean {
const doesMatch = this.patterns.some((pattern) => pattern.test(url.pathname));

return doesMatch;
}

getDefaultMetaData(url: URL, partial: Partial<MetaData> = {}): MetaData {
const urlObject = new URL(url);
const title = basename(urlObject.pathname);
public getDefaultMetaData(url: URL, partial: Partial<MetaData> = {}): MetaData {
const urlObject: URL = new URL(url);
const title: string = basename(urlObject.pathname);

return {
title,
description: '',
url: url.toString(),
type: 'unknown',
...partial,
title: partial.title ?? title,
description: partial.description ?? '',
url: partial.url ?? url.toString(),
type: partial.type ?? MetaDataEntityType.UNKNOWN,
};
}
}
Loading

0 comments on commit 879c6cd

Please sign in to comment.