diff --git a/src/app/shared/generated/.openapi-generator/FILES b/src/app/shared/generated/.openapi-generator/FILES index 80baf4a..ab5cf42 100644 --- a/src/app/shared/generated/.openapi-generator/FILES +++ b/src/app/shared/generated/.openapi-generator/FILES @@ -3,6 +3,7 @@ README.md api.module.ts api/api.ts +api/imagesInternal.service.ts api/themes.service.ts configuration.ts encoder.ts @@ -14,6 +15,7 @@ model/eximTheme.ts model/exportThemeRequest.ts model/getThemeResponse.ts model/getThemesResponse.ts +model/imageInfo.ts model/importThemeResponse.ts model/importThemeResponseStatus.ts model/models.ts @@ -21,6 +23,7 @@ model/pagingResponse.ts model/problemDetailInvalidParam.ts model/problemDetailParam.ts model/problemDetailResponse.ts +model/refType.ts model/searchThemeRequest.ts model/searchThemeResponse.ts model/theme.ts @@ -28,7 +31,6 @@ model/themeSnapshot.ts model/themeUpdateCreate.ts model/updateThemeRequest.ts model/updateThemeResponse.ts -model/validationConstraint.ts model/workspace.ts param.ts variables.ts diff --git a/src/app/shared/generated/api/api.ts b/src/app/shared/generated/api/api.ts index 78085e9..03e1a38 100644 --- a/src/app/shared/generated/api/api.ts +++ b/src/app/shared/generated/api/api.ts @@ -1,3 +1,5 @@ +export * from './imagesInternal.service'; +import { ImagesInternalAPIService } from './imagesInternal.service'; export * from './themes.service'; import { ThemesAPIService } from './themes.service'; -export const APIS = [ThemesAPIService]; +export const APIS = [ImagesInternalAPIService, ThemesAPIService]; diff --git a/src/app/shared/generated/api/imagesInternal.service.ts b/src/app/shared/generated/api/imagesInternal.service.ts new file mode 100644 index 0000000..3145fee --- /dev/null +++ b/src/app/shared/generated/api/imagesInternal.service.ts @@ -0,0 +1,335 @@ +/** + * onecx-theme-bff + * OneCx theme Bff + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { ImageInfo } from '../model/imageInfo'; +// @ts-ignore +import { ProblemDetailResponse } from '../model/problemDetailResponse'; +// @ts-ignore +import { RefType } from '../model/refType'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; + + +export interface GetImageRequestParams { + refId: string; + refType: RefType; +} + +export interface UpdateImageRequestParams { + refId: string; + refType: RefType; + body: Blob; + contentLength?: number; +} + +export interface UploadImageRequestParams { + contentLength: number; + refId: string; + refType: RefType; + body: Blob; +} + + +@Injectable({ + providedIn: 'any' +}) +export class ImagesInternalAPIService { + + protected basePath = 'http://onecx-theme-bff:8080'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + if (Array.isArray(basePath) && basePath.length > 0) { + basePath = basePath[0]; + } + + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + // @ts-ignore + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, (value as Date).toISOString().substring(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * Get Image by id + * @param requestParameters + * @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 getImage(requestParameters: GetImageRequestParams, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'image/*' | 'application/json', context?: HttpContext}): Observable; + public getImage(requestParameters: GetImageRequestParams, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'image/*' | 'application/json', context?: HttpContext}): Observable>; + public getImage(requestParameters: GetImageRequestParams, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'image/*' | 'application/json', context?: HttpContext}): Observable>; + public getImage(requestParameters: GetImageRequestParams, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'image/*' | 'application/json', context?: HttpContext}): Observable { + const refId = requestParameters.refId; + if (refId === null || refId === undefined) { + throw new Error('Required parameter refId was null or undefined when calling getImage.'); + } + const refType = requestParameters.refType; + if (refType === null || refType === undefined) { + throw new Error('Required parameter refType was null or undefined when calling getImage.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'image/*', + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + let localVarPath = `/images/${this.configuration.encodeParam({name: "refId", value: refId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/${this.configuration.encodeParam({name: "refType", value: refType, in: "path", style: "simple", explode: false, dataType: "RefType", dataFormat: undefined})}`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: "blob", + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + + /** + * update Images + * @param requestParameters + * @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 updateImage(requestParameters: UpdateImageRequestParams, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public updateImage(requestParameters: UpdateImageRequestParams, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public updateImage(requestParameters: UpdateImageRequestParams, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public updateImage(requestParameters: UpdateImageRequestParams, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + const refId = requestParameters.refId; + if (refId === null || refId === undefined) { + throw new Error('Required parameter refId was null or undefined when calling updateImage.'); + } + const refType = requestParameters.refType; + if (refType === null || refType === undefined) { + throw new Error('Required parameter refType was null or undefined when calling updateImage.'); + } + const body = requestParameters.body; + if (body === null || body === undefined) { + throw new Error('Required parameter body was null or undefined when calling updateImage.'); + } + const contentLength = requestParameters.contentLength; + + let localVarHeaders = this.defaultHeaders; + if (contentLength !== undefined && contentLength !== null) { + localVarHeaders = localVarHeaders.set('Content-Length', String(contentLength)); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'image/*' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/images/${this.configuration.encodeParam({name: "refId", value: refId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/${this.configuration.encodeParam({name: "refType", value: refType, in: "path", style: "simple", explode: false, dataType: "RefType", dataFormat: undefined})}`; + return this.httpClient.request('put', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: body, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + + /** + * Upload Images + * @param requestParameters + * @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 uploadImage(requestParameters: UploadImageRequestParams, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public uploadImage(requestParameters: UploadImageRequestParams, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public uploadImage(requestParameters: UploadImageRequestParams, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public uploadImage(requestParameters: UploadImageRequestParams, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + const contentLength = requestParameters.contentLength; + if (contentLength === null || contentLength === undefined) { + throw new Error('Required parameter contentLength was null or undefined when calling uploadImage.'); + } + const refId = requestParameters.refId; + if (refId === null || refId === undefined) { + throw new Error('Required parameter refId was null or undefined when calling uploadImage.'); + } + const refType = requestParameters.refType; + if (refType === null || refType === undefined) { + throw new Error('Required parameter refType was null or undefined when calling uploadImage.'); + } + const body = requestParameters.body; + if (body === null || body === undefined) { + throw new Error('Required parameter body was null or undefined when calling uploadImage.'); + } + + let localVarHeaders = this.defaultHeaders; + if (contentLength !== undefined && contentLength !== null) { + localVarHeaders = localVarHeaders.set('Content-Length', String(contentLength)); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'image/*' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/images/${this.configuration.encodeParam({name: "refId", value: refId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/${this.configuration.encodeParam({name: "refType", value: refType, in: "path", style: "simple", explode: false, dataType: "RefType", dataFormat: undefined})}`; + return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: body, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + +} diff --git a/src/app/shared/generated/model/validationConstraint.ts b/src/app/shared/generated/model/imageInfo.ts similarity index 76% rename from src/app/shared/generated/model/validationConstraint.ts rename to src/app/shared/generated/model/imageInfo.ts index 41f1cc1..cd188bf 100644 --- a/src/app/shared/generated/model/validationConstraint.ts +++ b/src/app/shared/generated/model/imageInfo.ts @@ -11,8 +11,7 @@ */ -export interface ValidationConstraint { - name?: string; - message?: string; +export interface ImageInfo { + id?: string; } diff --git a/src/app/shared/generated/model/models.ts b/src/app/shared/generated/model/models.ts index 7d2e4c1..11e705b 100644 --- a/src/app/shared/generated/model/models.ts +++ b/src/app/shared/generated/model/models.ts @@ -4,12 +4,14 @@ export * from './eximTheme'; export * from './exportThemeRequest'; export * from './getThemeResponse'; export * from './getThemesResponse'; +export * from './imageInfo'; export * from './importThemeResponse'; export * from './importThemeResponseStatus'; export * from './pagingResponse'; export * from './problemDetailInvalidParam'; export * from './problemDetailParam'; export * from './problemDetailResponse'; +export * from './refType'; export * from './searchThemeRequest'; export * from './searchThemeResponse'; export * from './theme'; @@ -17,5 +19,4 @@ export * from './themeSnapshot'; export * from './themeUpdateCreate'; export * from './updateThemeRequest'; export * from './updateThemeResponse'; -export * from './validationConstraint'; export * from './workspace'; diff --git a/src/app/shared/generated/model/refType.ts b/src/app/shared/generated/model/refType.ts new file mode 100644 index 0000000..1e65973 --- /dev/null +++ b/src/app/shared/generated/model/refType.ts @@ -0,0 +1,18 @@ +/** + * onecx-theme-bff + * OneCx theme Bff + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export enum RefType { + Logo = 'logo', + Favicon = 'favicon' +} + diff --git a/src/app/shared/generated/model/theme.ts b/src/app/shared/generated/model/theme.ts index ba43bd5..f272e60 100644 --- a/src/app/shared/generated/model/theme.ts +++ b/src/app/shared/generated/model/theme.ts @@ -12,7 +12,7 @@ export interface Theme { - version?: number; + modificationCount?: number; creationDate?: string; creationUser?: string; modificationDate?: string; diff --git a/src/app/shared/generated/model/themeUpdateCreate.ts b/src/app/shared/generated/model/themeUpdateCreate.ts index 9b490e0..93db243 100644 --- a/src/app/shared/generated/model/themeUpdateCreate.ts +++ b/src/app/shared/generated/model/themeUpdateCreate.ts @@ -12,7 +12,7 @@ export interface ThemeUpdateCreate { - version?: number; + modificationCount?: number; creationDate?: string; creationUser?: string; modificationDate?: string; diff --git a/src/app/shared/generated/model/workspace.ts b/src/app/shared/generated/model/workspace.ts index 32a5b67..74339b9 100644 --- a/src/app/shared/generated/model/workspace.ts +++ b/src/app/shared/generated/model/workspace.ts @@ -12,7 +12,7 @@ export interface Workspace { - workspaceName?: string; + name?: string; description?: string; } diff --git a/src/app/theme/theme-designer/theme-designer.component.html b/src/app/theme/theme-designer/theme-designer.component.html index 1cd0e82..8f1dd0f 100644 --- a/src/app/theme/theme-designer/theme-designer.component.html +++ b/src/app/theme/theme-designer/theme-designer.component.html @@ -80,7 +80,7 @@ accept=".png, .jpg, .jpeg" /> -
+
- +
-
+
- +
diff --git a/src/app/theme/theme-designer/theme-designer.component.spec.ts b/src/app/theme/theme-designer/theme-designer.component.spec.ts index 26f5010..fe8b5c5 100644 --- a/src/app/theme/theme-designer/theme-designer.component.spec.ts +++ b/src/app/theme/theme-designer/theme-designer.component.spec.ts @@ -259,25 +259,6 @@ describe('ThemeDesignerComponent', () => { expect(component.themeId).toBe('id') }) - it('should fetch logo and favicon from backend on edit mode when no http[s] present', () => { - const themeData = { - name: 'themeName', - logoUrl: 'logo_url', - faviconUrl: 'fav_url' - } - const themeResponse = { - resource: themeData - } - themeApiSpy.getThemeByName.and.returnValue(of(themeResponse) as any) - component.mode = 'EDIT' - component.themeName = 'themeName' - - component.ngOnInit() - - expect(component.fetchingLogoUrl).toBe(prepareUrl(themeData.logoUrl)) - expect(component.fetchingFaviconUrl).toBe(prepareUrl(themeData.faviconUrl)) - }) - it('should fetch logo and favicon from external source on edit mode when http[s] present', () => { const themeData = { logoUrl: 'http://myWeb.com/logo_url', @@ -967,8 +948,8 @@ describe('ThemeDesignerComponent', () => { general: jasmine.objectContaining({ 'primary-color': 'rgb(255,255,255)' }) }) ) - expect(component.fetchingFaviconUrl).toBe(prepareUrl('fetchedFavUrl')) - expect(component.fetchingLogoUrl).toBe(prepareUrl('fetchedLogoUrl')) + expect(component.fetchingFaviconUrl).toBe(undefined) + expect(component.fetchingLogoUrl).toBe(prepareUrl(undefined)) }) }) }) diff --git a/src/app/theme/theme-designer/theme-designer.component.ts b/src/app/theme/theme-designer/theme-designer.component.ts index 08ca5a0..9a62499 100644 --- a/src/app/theme/theme-designer/theme-designer.component.ts +++ b/src/app/theme/theme-designer/theme-designer.component.ts @@ -6,13 +6,17 @@ import { TranslateService } from '@ngx-translate/core' import { ConfirmationService, SelectItem } from 'primeng/api' import { Action, AppStateService, PortalMessageService, ThemeService } from '@onecx/portal-integration-angular' -import { dropDownSortItemsByLabel, dropDownGetLabelByValue, prepareUrl } from 'src/app/shared/utils' +import { dropDownSortItemsByLabel, dropDownGetLabelByValue } from 'src/app/shared/utils' import { + GetImageRequestParams, GetThemeResponse, + ImagesInternalAPIService, + RefType, Theme, ThemesAPIService, ThemeUpdateCreate, - UpdateThemeResponse + UpdateThemeResponse, + UploadImageRequestParams } from 'src/app/shared/generated' import { themeVariables } from './theme-variables' @@ -38,6 +42,8 @@ export class ThemeDesignerComponent implements OnInit { public themeIsCurrentUsedTheme = false public fetchingLogoUrl?: string public fetchingFaviconUrl?: string + public imageLogoExists: boolean | undefined + public imageFaviconExists: boolean | undefined public mode: 'EDIT' | 'NEW' = 'NEW' public autoApply = false @@ -64,7 +70,7 @@ export class ThemeDesignerComponent implements OnInit { private appStateService: AppStateService, private themeApi: ThemesAPIService, private themeService: ThemeService, - //private imageApi: ImageV1APIService, + private imageApi: ImagesInternalAPIService, private translate: TranslateService, private confirmation: ConfirmationService, private msgService: PortalMessageService @@ -144,16 +150,20 @@ export class ThemeDesignerComponent implements OnInit { } ngOnInit(): void { + this.imageLogoExists = false + this.imageFaviconExists = false if (this.mode === 'EDIT' && this.themeName) { this.themeApi.getThemeByName({ name: this.themeName }).subscribe((data) => { this.theme = data.resource this.basicForm.patchValue(data.resource) this.propertiesForm.reset() this.propertiesForm.patchValue(data.resource.properties || {}) - this.fetchingLogoUrl = prepareUrl(this.basicForm.value.logoUrl) - this.fetchingFaviconUrl = prepareUrl(this.basicForm.value.faviconUrl) + this.fetchingLogoUrl = this.getImageUrl(this.theme, 'logo') + this.fetchingFaviconUrl = this.getImageUrl(this.theme, 'favicon') this.themeId = data.resource.id this.themeIsCurrentUsedTheme = this.themeId === this.appStateService.currentPortal$.getValue()?.themeId + this.imageLogoExists = this.urlExists(data.resource.logoUrl) + this.imageFaviconExists = this.urlExists(data.resource.faviconUrl) }) } else { const currentVars: { [key: string]: { [key: string]: string } } = {} @@ -166,6 +176,7 @@ export class ThemeDesignerComponent implements OnInit { this.propertiesForm.reset() this.propertiesForm.patchValue(currentVars) } + this.loadThemeTemplates() } @@ -253,10 +264,10 @@ export class ThemeDesignerComponent implements OnInit { if (this.mode === 'NEW') { this.basicForm.controls['name'].setValue(data['GENERAL.COPY_OF'] + result.resource.name) this.basicForm.controls['description'].setValue(result.resource.description) - this.basicForm.controls['faviconUrl'].setValue(result.resource.faviconUrl) - this.basicForm.controls['logoUrl'].setValue(result.resource.logoUrl) - this.fetchingLogoUrl = prepareUrl(this.basicForm.value.logoUrl) - this.fetchingFaviconUrl = prepareUrl(this.basicForm.value.faviconUrl) + this.basicForm.controls['faviconUrl'].setValue(this.getImageUrl(result.resource, 'favicon')) + this.basicForm.controls['logoUrl'].setValue(this.getImageUrl(result.resource, 'logo')) + this.fetchingLogoUrl = this.getImageUrl(this.theme, 'logo') + this.fetchingFaviconUrl = this.getImageUrl(this.theme, 'favicon') } if (result.resource.properties) { this.propertiesForm.reset() @@ -299,6 +310,16 @@ export class ThemeDesignerComponent implements OnInit { .pipe( switchMap((data) => { data.resource.properties = this.propertiesForm.value + if (this.imageFaviconExists) { + data.resource.faviconUrl = undefined + } else { + data.resource.faviconUrl = this.basicForm.controls['faviconUrl'].value + } + if (this.imageLogoExists) { + data.resource.logoUrl = undefined + } else { + data.resource.faviconUrl = this.basicForm.controls['logoUrl'].value + } Object.assign(data.resource, this.basicForm.value) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.themeApi.updateTheme({ @@ -327,6 +348,12 @@ export class ThemeDesignerComponent implements OnInit { const newTheme: ThemeUpdateCreate = { ...this.basicForm.value } newTheme.name = newThemename newTheme.properties = this.propertiesForm.value + if (this.imageFaviconExists) { + newTheme.faviconUrl = undefined + } + if (this.imageLogoExists) { + newTheme.logoUrl = undefined + } this.themeApi.createTheme({ createThemeRequest: { resource: newTheme } }).subscribe({ next: (data) => { @@ -369,21 +396,101 @@ export class ThemeDesignerComponent implements OnInit { return this.themeApi.getThemeById({ id: id }) } + // Applying Styles + private updateCssVar(varName: string, value: string): void { + document.documentElement.style.setProperty(`--${varName}`, value) + const rgb = this.hexToRgb(value) + if (rgb) { + document.documentElement.style.setProperty(`--${varName}-rgb`, `${rgb.r},${rgb.g},${rgb.b}`) + } + } + + private hexToRgb(hex: string): { r: number; g: number; b: number } | null { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } + : null + } + // Image Files public onFileUpload(ev: Event, fieldType: 'logo' | 'favicon'): void { + var currThemeName = this.basicForm.controls['name'].value this.displayFileTypeErrorLogo = false this.displayFileTypeErrorFavicon = false - /** if (ev.target && (ev.target as HTMLInputElement).files) { const files = (ev.target as HTMLInputElement).files - if (files) { + + if (files && files[0].size > 110000) { + this.msgService.error({ summaryKey: 'ACTIONS.EDIT.MESSAGE.IMAGE_CONSTRAINT_SIZE' }) + } else if (files && currThemeName) { + this.saveImage(currThemeName, fieldType, files) + } else { + this.msgService.error({ summaryKey: 'ACTIONS.EDIT.MESSAGE.IMAGE_CONSTRAINT' }) + } + } + } + + saveImage(currThemeName: string, fieldType: string, files: FileList) { + // Set request parameter + var requestParameters: UploadImageRequestParams + const blob = new Blob([files[0]], { type: files[0].type }) + var imageType: RefType + if (fieldType === 'logo') { + this.fetchingLogoUrl = undefined + imageType = RefType.Logo + } else { + this.fetchingFaviconUrl = undefined + imageType = RefType.Favicon + } + requestParameters = { + contentLength: files.length, + refId: currThemeName!, + refType: imageType, + body: blob + } + + var requestParametersGet: GetImageRequestParams + requestParametersGet = { + refId: currThemeName!, + refType: imageType + } + + this.imageApi.getImage(requestParametersGet).subscribe( + (res) => { + if (files[0].name.match(/^.*.(jpg|jpeg|png)$/)) { + this.imageApi.updateImage(requestParameters).subscribe((data) => { + if (fieldType == 'logo') { + this.imageLogoExists = true + this.fetchingLogoUrl = this.imageApi.configuration.basePath + '/images/' + currThemeName + '/' + fieldType + } else { + this.imageFaviconExists = true + this.fetchingFaviconUrl = + this.imageApi.configuration.basePath + '/images/' + currThemeName + '/' + fieldType + } + this.msgService.info({ summaryKey: 'LOGO.UPLOADED' }) + this.basicForm.controls[fieldType + 'Url'].setValue('') + }) + } + }, + (err) => { if (files[0].name.match(/^.*.(jpg|jpeg|png)$/)) { Array.from(files).forEach((file) => { - this.imageApi.uploadImage({ image: file }).subscribe((data) => { - this.basicForm.controls[fieldType + 'Url'].setValue(data.imageUrl) - this.fetchingLogoUrl = prepareUrl(this.basicForm.value.logoUrl) - this.fetchingFaviconUrl = prepareUrl(this.basicForm.value.faviconUrl) + this.imageApi.uploadImage(requestParameters).subscribe((data) => { + if (fieldType == 'logo') { + this.imageLogoExists = true + this.fetchingLogoUrl = + this.imageApi.configuration.basePath + '/images/' + currThemeName + '/' + fieldType + } else { + this.imageFaviconExists = true + this.fetchingFaviconUrl = + this.imageApi.configuration.basePath + '/images/' + currThemeName + '/' + fieldType + } this.msgService.info({ summaryKey: 'LOGO.UPLOADED' }) + this.basicForm.controls[fieldType + 'Url'].setValue('') }) }) } else { @@ -391,27 +498,55 @@ export class ThemeDesignerComponent implements OnInit { this.displayFileTypeErrorFavicon = fieldType === 'favicon' } } + ) + } + + constraintUpload(): boolean { + var currThemeName = this.basicForm.controls['name'].value + if (currThemeName == null || currThemeName == '') { + return false } - */ + return true } - // Applying Styles - private updateCssVar(varName: string, value: string): void { - document.documentElement.style.setProperty(`--${varName}`, value) - const rgb = this.hexToRgb(value) - if (rgb) { - document.documentElement.style.setProperty(`--${varName}-rgb`, `${rgb.r},${rgb.g},${rgb.b}`) + getImageUrl(theme: Theme | undefined, imageType: string): string | undefined { + if (!theme) { + return undefined + } + if (imageType == 'logo') { + if (theme.logoUrl != null && theme.logoUrl != '') { + return theme.logoUrl + } + return this.imageApi.configuration.basePath + '/images/' + theme.name + '/logo' + } else { + if (theme.faviconUrl != null && theme.faviconUrl != '') { + return theme.faviconUrl + } + return this.imageApi.configuration.basePath + '/images/' + theme.name + '/favicon' } } - private hexToRgb(hex: string): { r: number; g: number; b: number } | null { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) - return result - ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } - : null + urlExists(url: string | undefined): boolean { + if (url == undefined || url === '') { + return true + } + return false + } + + inputChange(theme: Theme | undefined, fieldType: string) { + setTimeout(() => { + if (fieldType == 'logo') { + this.fetchingLogoUrl = this.basicForm.controls['logoUrl'].value + } else { + this.fetchingFaviconUrl = this.basicForm.controls['faviconUrl'].value + } + + if (this.imageLogoExists && this.basicForm.controls['logoUrl'].value == '') { + this.fetchingLogoUrl = this.imageApi.configuration.basePath + '/images/' + theme?.name + '/logo' + } + if (this.imageFaviconExists && this.basicForm.controls['faviconUrl'].value == '') { + this.fetchingFaviconUrl = this.imageApi.configuration.basePath + '/images/' + theme?.name + '/favicon' + } + }, 1000) } } diff --git a/src/app/theme/theme-detail/theme-detail.component.html b/src/app/theme/theme-detail/theme-detail.component.html index 701f1cb..574d28c 100644 --- a/src/app/theme/theme-detail/theme-detail.component.html +++ b/src/app/theme/theme-detail/theme-detail.component.html @@ -36,9 +36,9 @@
diff --git a/src/app/theme/theme-detail/theme-detail.component.spec.ts b/src/app/theme/theme-detail/theme-detail.component.spec.ts index f9d18c5..d9dcb64 100644 --- a/src/app/theme/theme-detail/theme-detail.component.spec.ts +++ b/src/app/theme/theme-detail/theme-detail.component.spec.ts @@ -11,7 +11,6 @@ import FileSaver from 'file-saver' import { ConfigurationService, PortalMessageService } from '@onecx/portal-integration-angular' -import { prepareUrl } from 'src/app/shared/utils' import { ThemesAPIService } from 'src/app/shared/generated' import { ThemeDetailComponent } from './theme-detail.component' @@ -112,7 +111,7 @@ describe('ThemeDetailComponent', () => { }, workspaces: [ { - workspaceName: 'workspace', + name: 'workspace', description: 'workspaceDesc' } ] @@ -188,10 +187,10 @@ describe('ThemeDetailComponent', () => { }, workspaces: [ { - workspaceName: 'portal1' + name: 'portal1' }, { - workspaceName: 'myPortal' + name: 'myPortal' } ] } @@ -307,7 +306,7 @@ describe('ThemeDetailComponent', () => { await component.ngOnInit() - expect(component.headerImageUrl).toBe(prepareUrl('logo123.png')) + expect(component.headerImageUrl).toBe('logo123.png') }) it('should set header image url without prefix when theme logo has http/https', async () => { @@ -375,7 +374,7 @@ describe('ThemeDetailComponent', () => { ) component.theme = { - version: 1, + modificationCount: 1, name: 'themeName', logoUrl: 'url', diff --git a/src/app/theme/theme-detail/theme-detail.component.ts b/src/app/theme/theme-detail/theme-detail.component.ts index bfe4d0d..00905bc 100644 --- a/src/app/theme/theme-detail/theme-detail.component.ts +++ b/src/app/theme/theme-detail/theme-detail.component.ts @@ -7,8 +7,14 @@ import FileSaver from 'file-saver' import { Action, ObjectDetailItem, PortalMessageService, UserService } from '@onecx/portal-integration-angular' -import { limitText, prepareUrl, sortByLocale } from 'src/app/shared/utils' -import { ExportThemeRequest, Theme, ThemesAPIService, Workspace } from 'src/app/shared/generated' +import { limitText, sortByLocale } from 'src/app/shared/utils' +import { + ExportThemeRequest, + ImagesInternalAPIService, + Theme, + ThemesAPIService, + Workspace +} from 'src/app/shared/generated' @Component({ templateUrl: './theme-detail.component.html', @@ -34,7 +40,8 @@ export class ThemeDetailComponent implements OnInit { private route: ActivatedRoute, private themeApi: ThemesAPIService, private msgService: PortalMessageService, - private translate: TranslateService + private translate: TranslateService, + private imageApi: ImagesInternalAPIService ) { this.themeName = this.route.snapshot.paramMap.get('name') || '' this.dateFormat = this.user.lang$.getValue() === 'de' ? 'dd.MM.yyyy HH:mm:ss' : 'medium' @@ -53,7 +60,7 @@ export class ThemeDetailComponent implements OnInit { this.theme = data.resource this.usedInWorkspace = data.workspaces this.preparePage() - this.headerImageUrl = prepareUrl(this.theme?.logoUrl) + this.headerImageUrl = this.getLogoUrl(this.theme) }, error: (err) => { this.msgService.error({ @@ -187,7 +194,6 @@ export class ThemeDetailComponent implements OnInit { onExportTheme(): void { if (this.theme?.name) { const exportThemeRequest: ExportThemeRequest = { names: [this.theme.name] } - console.log(exportThemeRequest) this.themeApi .exportThemes({ exportThemeRequest @@ -206,7 +212,17 @@ export class ThemeDetailComponent implements OnInit { } public prepareUsedInPortalList(): string { - const arr = this.usedInWorkspace?.map((workspace: Workspace) => workspace.workspaceName) + const arr = this.usedInWorkspace?.map((workspace: Workspace) => workspace.name) return arr?.sort(sortByLocale).join(', ') ?? '' } + + getLogoUrl(theme: Theme | undefined): string | undefined { + if (!theme) { + return undefined + } + if (theme.logoUrl != null && theme.logoUrl != '') { + return theme.logoUrl + } + return this.imageApi.configuration.basePath + '/images/' + theme.name + '/logo' + } } diff --git a/src/app/theme/theme-search/theme-search.component.html b/src/app/theme/theme-search/theme-search.component.html index a7e6ee2..6d61d6b 100644 --- a/src/app/theme/theme-search/theme-search.component.html +++ b/src/app/theme/theme-search/theme-search.component.html @@ -49,7 +49,7 @@
@@ -74,7 +74,7 @@
- +
diff --git a/src/app/theme/theme-search/theme-search.component.ts b/src/app/theme/theme-search/theme-search.component.ts index e78c458..1be8503 100644 --- a/src/app/theme/theme-search/theme-search.component.ts +++ b/src/app/theme/theme-search/theme-search.component.ts @@ -6,7 +6,7 @@ import { DataView } from 'primeng/dataview' import { Action, DataViewControlTranslations } from '@onecx/portal-integration-angular' -import { GetThemesResponse, ThemesAPIService } from 'src/app/shared/generated' +import { GetThemesResponse, ImagesInternalAPIService, Theme, ThemesAPIService } from 'src/app/shared/generated' import { limitText } from 'src/app/shared/utils' @Component({ @@ -30,7 +30,8 @@ export class ThemeSearchComponent implements OnInit { private route: ActivatedRoute, private router: Router, private themeApi: ThemesAPIService, - private translate: TranslateService + private translate: TranslateService, + private imageApi: ImagesInternalAPIService ) {} ngOnInit(): void { @@ -101,6 +102,16 @@ export class ThemeSearchComponent implements OnInit { } } + getLogoUrl(theme: Theme | undefined): string | undefined { + if (!theme) { + return undefined + } + if (theme.logoUrl != null && theme.logoUrl != '') { + return theme.logoUrl + } + return this.imageApi.configuration.basePath + '/images/' + theme.name + '/logo' + } + public onNewTheme(): void { this.router.navigate(['./new'], { relativeTo: this.route }) } diff --git a/src/assets/api/themes-bff-api.yaml b/src/assets/api/themes-bff-api.yaml index 58b7289..546ff63 100644 --- a/src/assets/api/themes-bff-api.yaml +++ b/src/assets/api/themes-bff-api.yaml @@ -8,9 +8,14 @@ servers: - url: http://onecx-theme-bff:8080/ tags: - name: theme + - name: imagesInternal paths: /themes: get: + x-onecx: + permissions: + themes: + - read tags: - themes description: Find all themes @@ -34,6 +39,10 @@ paths: '404': description: Not found post: + x-onecx: + permissions: + themes: + - write tags: - themes description: Create theme @@ -62,6 +71,10 @@ paths: description: Not Found /themes/{id}: get: + x-onecx: + permissions: + themes: + - read tags: - themes description: Find theme by id @@ -84,6 +97,10 @@ paths: '404': description: Not Found put: + x-onecx: + permissions: + themes: + - write tags: - themes description: Update theme @@ -113,6 +130,10 @@ paths: '404': description: Not Found delete: + x-onecx: + permissions: + themes: + - delete tags: - themes description: Delete theme @@ -130,6 +151,10 @@ paths: $ref: '#/components/schemas/ProblemDetailResponse' /themes/name/{name}: get: + x-onecx: + permissions: + themes: + - read tags: - themes description: Find theme by name including workspace @@ -153,6 +178,10 @@ paths: description: Not Found /themes/search: post: + x-onecx: + permissions: + themes: + - read tags: - themes description: Search themes by criteria @@ -182,6 +211,10 @@ paths: description: Not Found /themes/export: post: + x-onecx: + permissions: + themes: + - read tags: - themes description: Export list of themes @@ -203,6 +236,10 @@ paths: description: No themes founds /themes/import: post: + x-onecx: + permissions: + themes: + - write tags: - themes description: Import themes @@ -226,12 +263,139 @@ paths: application/json: schema: $ref: '#/components/schemas/ProblemDetailResponse' + /images/{refId}/{refType}: + post: + x-onecx: + permissions: + themes: + - write + tags: + - imagesInternal + description: Upload Images + parameters: + - in: header + name: Content-Length + required: true + schema: + minimum: 1 + maximum: 20000 + type: integer + - name: refId + in: path + required: true + schema: + type: string + - name: refType + in: path + required: true + schema: + $ref: '#/components/schemas/RefType' + operationId: uploadImage + requestBody: + required: true + content: + image/*: + schema: + type: string + format: binary + responses: + '201': + description: CREATED + content: + application/json: + schema: + $ref: '#/components/schemas/ImageInfo' + '400': + description: Bad Request + get: + x-onecx: + permissions: + themes: + - read + tags: + - imagesInternal + description: Get Image by id + operationId: getImage + parameters: + - name: refId + in: path + required: true + schema: + type: string + - name: refType + in: path + required: true + schema: + $ref: '#/components/schemas/RefType' + responses: + '200': + description: OK + content: + image/*: + schema: + type: string + format: binary + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + put: + x-onecx: + permissions: + themes: + - write + tags: + - imagesInternal + description: update Images + operationId: updateImage + parameters: + - in: header + name: Content-Length + schema: + type: integer + minimum: 1 + maximum: 20000 + - name: refId + in: path + required: true + schema: + type: string + - name: refType + in: path + required: true + schema: + $ref: '#/components/schemas/RefType' + requestBody: + required: true + content: + image/*: + schema: + type: string + format: binary + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ImageInfo' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' components: schemas: + RefType: + type: string + enum: [logo, favicon] Theme: type: object properties: - version: + modificationCount: format: int32 type: integer creationDate: @@ -266,7 +430,7 @@ components: ThemeUpdateCreate: type: object properties: - version: + modificationCount: format: int32 type: integer creationDate: @@ -364,13 +528,6 @@ components: type: string message: type: string - ValidationConstraint: - type: object - properties: - name: - type: string - message: - type: string CreateThemeRequest: required: - resource @@ -424,7 +581,7 @@ components: Workspace: type: object properties: - workspaceName: + name: type: string description: type: string @@ -485,6 +642,11 @@ components: type: string properties: type: object + ImageInfo: + type: object + properties: + id: + type: string parameters: pageNumber: in: query diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index e7f0077..a260f9c 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -28,7 +28,9 @@ "TOOLTIP": "Die Eigenschaften bearbeiten", "MESSAGE": { "CHANGE_OK": "Das Theme wurde erfolgreich geändert", - "CHANGE_NOK": "Ein Fehler ist aufgetreten. Das Theme wurde nicht geändert." + "CHANGE_NOK": "Ein Fehler ist aufgetreten. Das Theme wurde nicht geändert.", + "IMAGE_CONSTRAINT": "Ein Fehler ist aufgetreten. Bitte füge einen Namen hinzu bevor das Bild geladen wird.", + "IMAGE_CONSTRAINT_SIZE": "Ein Fehler ist aufgetreten. Das Bild ist zu groß." } }, "EXPORT": { diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 1a98407..b7cab4b 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -28,7 +28,9 @@ "TOOLTIP": "Edit properties", "MESSAGE": { "CHANGE_OK": "The Theme was changed successfully", - "CHANGE_NOK": "An error has occurred. The Theme was not changed." + "CHANGE_NOK": "An error has occurred. The Theme was not changed.", + "IMAGE_CONSTRAINT": "An error has occurred. Please add a theme name before uploading a file.", + "IMAGE_CONSTRAINT_SIZE": "An error has occurred. The image is too large." } }, "EXPORT": {