diff --git a/src/api/backend/api/default.service.ts b/src/api/backend/api/default.service.ts index 246f9fd0..4016a5ef 100644 --- a/src/api/backend/api/default.service.ts +++ b/src/api/backend/api/default.service.ts @@ -112,6 +112,8 @@ export class DefaultService { this.configuration = configuration; this.basePath = basePath || configuration.basePath || this.basePath; } + + this.basePath = this.basePath.replace(/\/+$/, ''); // remove trailing slash } /** @@ -3993,51 +3995,53 @@ export class DefaultService { * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public taskContent2GetSingle(view: string, contentId: string, observe?: 'body', reportProgress?: boolean): Observable; - public taskContent2GetSingle(view: string, contentId: string, observe?: 'response', reportProgress?: boolean): Observable>; - public taskContent2GetSingle(view: string, contentId: string, observe?: 'events', reportProgress?: boolean): Observable>; - public taskContent2GetSingle(view: string, contentId: string, observe: any = 'body', reportProgress: boolean = false ): Observable { - - if (view === null || view === undefined) { - throw new Error('Required parameter view was null or undefined when calling taskContent2GetSingle.'); - } - - if (contentId === null || contentId === undefined) { - throw new Error('Required parameter contentId was null or undefined when calling taskContent2GetSingle.'); - } - - let headers = this.defaultHeaders; - - // authentication (ksi) required - if (this.configuration.accessToken) { - const accessToken = typeof this.configuration.accessToken === 'function' - ? this.configuration.accessToken() - : this.configuration.accessToken; - headers = headers.set('Authorization', 'Bearer ' + accessToken); - } - - // to determine the Accept header - let httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); - if (httpHeaderAcceptSelected != undefined) { - headers = headers.set('Accept', httpHeaderAcceptSelected); - } - - // to determine the Content-Type header - const consumes: string[] = [ - ]; - - return this.httpClient.request('get',`${this.basePath}/taskContent/${encodeURIComponent(String(contentId))}/${encodeURIComponent(String(view))}`, - { - withCredentials: this.configuration.withCredentials, - headers: headers, - observe: observe, - reportProgress: reportProgress - } - ); - } + public taskContent2GetSingle(view: string, contentId: string, observe?: 'body', reportProgress?: boolean): Observable; + public taskContent2GetSingle(view: string, contentId: string, observe?: 'response', reportProgress?: boolean): Observable>; + public taskContent2GetSingle(view: string, contentId: string, observe?: 'events', reportProgress?: boolean): Observable>; + public taskContent2GetSingle(view: string, contentId: string, observe: any = 'body', reportProgress: boolean = false ): Observable { + + if (view === null || view === undefined) { + throw new Error('Required parameter view was null or undefined when calling taskContent2GetSingle.'); + } + + if (contentId === null || contentId === undefined) { + throw new Error('Required parameter contentId was null or undefined when calling taskContent2GetSingle.'); + } + + let headers = this.defaultHeaders; + + // authentication (ksi) required + if (this.configuration.accessToken) { + const accessToken = typeof this.configuration.accessToken === 'function' + ? this.configuration.accessToken() + : this.configuration.accessToken; + headers = headers.set('Authorization', 'Bearer ' + accessToken); + } + + // to determine the Accept header + let httpHeaderAccepts: string[] = [ + 'application/json' + ]; + const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + if (httpHeaderAcceptSelected != undefined) { + headers = headers.set('Accept', httpHeaderAcceptSelected); + } + + // to determine the Content-Type header + const consumes: string[] = [ + ]; + + return this.httpClient.request('get',`${this.basePath}/taskContent/${String(contentId)}/${String(view)}`, + { + //@ts-ignore + responseType: "blob", + withCredentials: this.configuration.withCredentials, + headers: headers, + observe: observe, + reportProgress: reportProgress + } + ); + } /** * @@ -5221,4 +5225,4 @@ export class DefaultService { ); } -} +} \ No newline at end of file diff --git a/src/app/services/tasks/tasks.service.ts b/src/app/services/tasks/tasks.service.ts index 76e35ece..7096b5bf 100644 --- a/src/app/services/tasks/tasks.service.ts +++ b/src/app/services/tasks/tasks.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; import { BackendService, YearsService, UserService } from '../shared'; import { combineLatest, merge, Observable, of, Subject } from 'rxjs'; import { filter, map, mapTo, mergeMap, shareReplay, take, tap } from 'rxjs/operators'; @@ -38,13 +39,20 @@ export class TasksService { private readonly taskUpdatesSubject: Subject = new Subject(); private readonly taskUpdates$ = this.taskUpdatesSubject.asObservable(); - constructor(private backend: BackendService, private year: YearsService, private userService: UserService) { + constructor( + private backend: BackendService, + private year: YearsService, + private userService: UserService, + private sanitizer: DomSanitizer, + ) { this.tasks$ = merge( backend.user$.pipe(map(() => this.year.selected)), year.selected$ ).pipe( mergeMap((year) => this.backend.http.tasksGetAll(year?.id)), - map((response) => response.tasks.map((task) => TasksService.taskAddIcon(task))), + mergeMap((response) => + combineLatest(response.tasks.map((task) => this.taskAddIcon(task))), + ), tap((tasks) => { tasks.forEach((task) => this.updateTask(task)); }), @@ -156,7 +164,8 @@ export class TasksService { } return this.backend.http.tasksGetSingle(taskId).pipe( - map((response) => TasksService.taskAddIcon(response.task)), + map((response) => this.taskAddIcon(response.task)), + mergeMap((taskWithIcon$) => taskWithIcon$), tap((task) => this.updateTask(task, publishUpdate)), take(1), ); @@ -168,26 +177,32 @@ export class TasksService { * @param publishUpdate if set to false then no task update subscribers are notified upon update */ public updateTask(task: Task, publishUpdate = true): void { - environment.logger.debug(`[TASK] updating task cache for task ${task.id}, publish update: ${publishUpdate}`); - const taskWithIcon = TasksService.taskAddIcon(task); - - /* - Empty space in the cache - */ - const cachedKeys = Object.keys(this.cache); - let firstKey; - while (cachedKeys.length >= TasksService.CACHE_MAX_SIZE && (firstKey = cachedKeys.shift())) { - const key = Number(firstKey); - delete this.cache[key]; - } + environment.logger.debug( + `[TASK] updating task cache for task ${task.id}, publish update: ${publishUpdate}`, + ); - /* - Save the task into cache - */ - this.cache[taskWithIcon.id] = taskWithIcon; - if (publishUpdate) { - this.taskUpdatesSubject.next(taskWithIcon); - } + this.taskAddIcon(task).subscribe((taskWithIcon) => { + /* + Empty space in the cache + */ + const cachedKeys = Object.keys(this.cache); + let firstKey; + while ( + cachedKeys.length >= TasksService.CACHE_MAX_SIZE && + (firstKey = cachedKeys.shift()) + ) { + const key = Number(firstKey); + delete this.cache[key]; + } + + /* + Save the task into cache + */ + this.cache[taskWithIcon.id] = taskWithIcon; + if (publishUpdate) { + this.taskUpdatesSubject.next(taskWithIcon); + } + }); } /** @@ -236,6 +251,7 @@ export class TasksService { mapTo(true)); } + public static sortTasks(tasks: TaskWithIcon[], lockedLast: boolean): TaskWithIcon[] { const flatLevels = Utils.flatArray(TasksService.splitToLevels(tasks)); if (!lockedLast) { @@ -302,10 +318,24 @@ export class TasksService { return r; } - private static taskAddIcon(task: Task): TaskWithIcon { - return { - ...task, - icon: Utils.getTaskIconURL(task), - }; + + private taskAddIcon(task: Task): Observable { + return this.backend.http + .taskContent2GetSingle( + `icon/${task.state}${task.picture_suffix}`, + task.id.toString(), + ) + .pipe( + mergeMap((icon) => { + const url = URL.createObjectURL(icon); + // icon is safe + const safeUrl = this.sanitizer.bypassSecurityTrustUrl(url); + + return of({ + ...task, + icon: safeUrl as string, + }); + }), + ); } } diff --git a/src/app/util/utils.ts b/src/app/util/utils.ts index 058ad1f8..98c74b11 100644 --- a/src/app/util/utils.ts +++ b/src/app/util/utils.ts @@ -60,10 +60,6 @@ export class Utils { return r; } - public static getTaskIconURL(task: Task): string { - return Utils.fixUrl(`${environment.backend}${task.picture_base}/${task.state}${task.picture_suffix}`); - } - public static fixUrl(url: string): string { const replace = (x: string): string => x.replace(/([^:])\/\//g, '$1/'); return replace(replace(url));