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-2358 Link elements display card title for board card links #5467

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

Filter by extension

Filter by extension

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