UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1 lines • 162 kB
{"version":3,"file":"c8y-ngx-components-branding-shared-lazy.mjs","sources":["../../branding/shared/lazy/apply-branding-to-app-modal/apply-branding-to-app.service.ts","../../branding/shared/lazy/apply-branding-to-app-modal/apply-branding-to-app-modal.component.ts","../../branding/shared/lazy/apply-branding-to-app-modal/apply-branding-to-app-modal.component.html","../../branding/shared/lazy/branding-tags-cell-renderer/branding-tags-cell-renderer.component.ts","../../branding/shared/lazy/branding-tags-cell-renderer/branding-tags-cell-renderer.component.html","../../branding/shared/lazy/branding-import-modal/branding-import-modal.component.ts","../../branding/shared/lazy/branding-import-modal/branding-import-modal.component.html","../../branding/shared/lazy/branding-import-modal/branding-import-modal.service.ts","../../branding/shared/lazy/branding-name-cell-renderer/branding-name-cell-renderer.component.ts","../../branding/shared/lazy/branding-name-cell-renderer/branding-name-cell-renderer.component.html","../../branding/shared/lazy/branding-tags-header-cell-renderer/branding-tags-header-cell-renderer.component.ts","../../branding/shared/lazy/branding-tags-header-cell-renderer/branding-tags-header-cell-renderer.component.html","../../branding/shared/lazy/branding/branding.component.ts","../../branding/shared/lazy/branding/branding.component.html","../../branding/shared/lazy/branding-form/branding-form.component.ts","../../branding/shared/lazy/branding-form/branding-form.component.html","../../branding/shared/lazy/branding-theme-form/branding-theme-form-structure.ts","../../branding/shared/lazy/branding-theme-form/branding-theme-form.component.ts","../../branding/shared/lazy/branding-theme-form/branding-theme-form.component.html","../../branding/shared/lazy/edit-branding-router-outlet/edit-branding-router-outlet.component.ts","../../branding/shared/lazy/edit-branding-router-outlet/edit-branding-router-outlet.component.html","../../branding/shared/lazy/branding-assets/branding-assets.component.ts","../../branding/shared/lazy/branding-assets/branding-assets.component.html","../../branding/shared/lazy/c8y-ngx-components-branding-shared-lazy.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\nimport { ApplicationService, ApplicationType, IApplication } from '@c8y/client';\n\n@Injectable({ providedIn: 'root' })\nexport class ApplyBrandingToAppService {\n constructor(private apps: ApplicationService) {}\n\n async getBrandableApps(): Promise<IApplication[]> {\n const { data: apps } = await this.apps.list({\n pageSize: 2000,\n dropOverwrittenApps: true,\n type: ApplicationType.HOSTED\n });\n const appsWithDynamicOptionsUrl = this.getHostedAppsWhereDynamicOptionsUrlIsUndefined(apps);\n return appsWithDynamicOptionsUrl;\n }\n\n getHostedAppsWhereDynamicOptionsUrlIsUndefined(apps: IApplication[]) {\n return apps.filter(app => {\n const manifest = app.manifest;\n if (!manifest) {\n return false;\n }\n // filter out none web sdk based apps\n if (!manifest.webSdkVersion) {\n return false;\n }\n // filter out packages\n if (manifest.isPackage) {\n return false;\n }\n const dynamicOptionsUrl = manifest.dynamicOptionsUrl;\n\n return dynamicOptionsUrl === undefined || dynamicOptionsUrl === true;\n });\n }\n}\n","import { Component, Input, OnInit } from '@angular/core';\nimport { ApplyBrandingToAppService } from './apply-branding-to-app.service';\nimport { FormBuilder, FormControl, ReactiveFormsModule } from '@angular/forms';\nimport { AsyncPipe, NgForOf, NgIf } from '@angular/common';\nimport {\n AppIconComponent,\n C8yTranslateDirective,\n C8yTranslatePipe,\n HumanizeAppNamePipe,\n ListItemBodyComponent,\n ListItemCheckboxComponent,\n ListItemComponent,\n ListItemIconComponent,\n ModalComponent\n} from '@c8y/ngx-components';\nimport { Observable, merge, of } from 'rxjs';\nimport { map } from 'rxjs/operators';\nimport { uniqBy } from 'lodash-es';\n\n@Component({\n selector: 'c8y-apply-branding-to-app-modal',\n templateUrl: './apply-branding-to-app-modal.component.html',\n standalone: true,\n imports: [\n NgIf,\n AsyncPipe,\n ReactiveFormsModule,\n ModalComponent,\n C8yTranslatePipe,\n C8yTranslateDirective,\n ListItemComponent,\n ListItemIconComponent,\n ListItemCheckboxComponent,\n AppIconComponent,\n ListItemBodyComponent,\n HumanizeAppNamePipe,\n NgForOf\n ]\n})\nexport class ApplyBrandingToAppModalComponent implements OnInit {\n @Input() currentTags: string[];\n brandableApps: Awaited<ReturnType<ApplyBrandingToAppService['getBrandableApps']>> = [];\n form: ReturnType<ApplyBrandingToAppModalComponent['initForm']>;\n numberOfSelectedApps$: Observable<number>;\n\n result = new Promise<ReturnType<typeof this.initForm>['value']>((resolve, reject) => {\n this._resovle = resolve;\n this._reject = reject;\n });\n\n private _resovle: (value: ReturnType<typeof this.initForm>['value']) => void;\n private _reject: (reason?: any) => void;\n constructor(\n private apply: ApplyBrandingToAppService,\n private formBuilder: FormBuilder\n ) {}\n\n async ngOnInit() {\n const brandableApps = await this.apply.getBrandableApps();\n this.brandableApps = uniqBy(brandableApps, 'contextPath');\n const tags = this.brandableApps.map(app => app.contextPath);\n this.form = this.initForm(tags);\n if (this.currentTags.length) {\n this.form.patchValue(\n this.currentTags.reduce((prev, curr) => Object.assign(prev, { [curr]: true }), {})\n );\n }\n this.numberOfSelectedApps$ = merge(this.form.valueChanges, of(this.form.value)).pipe(\n map(value => Object.values(value).filter(Boolean).length)\n );\n }\n\n save() {\n this._resovle(this.form.value);\n }\n\n cancel() {\n this._reject();\n }\n\n private initForm(tags: string[]) {\n const tagsFormMap: Record<string, FormControl<boolean>> = tags.reduceRight(\n (prev, curr) => Object.assign(prev, { [curr]: this.formBuilder.control(false) }),\n {}\n );\n return this.formBuilder.group(tagsFormMap);\n }\n}\n","<c8y-modal\n [title]=\"'Apply branding to apps' | translate\"\n [disabled]=\"!form || form.invalid || !brandableApps.length\"\n [headerClasses]=\"'dialog-header'\"\n (onDismiss)=\"cancel()\"\n (onClose)=\"save()\"\n [labels]=\"{ cancel: 'Cancel', ok: 'Save' }\"\n>\n <ng-container c8y-modal-title>\n <span c8yIcon=\"palette\"></span>\n </ng-container>\n <c8y-list-group\n class=\"m-b-0 no-border-last\"\n *ngIf=\"form\"\n [formGroup]=\"form\"\n >\n <c8y-li>\n <p\n class=\"text-center text-medium\"\n *ngIf=\"numberOfSelectedApps$ | async as numberOfApps; else noapps\"\n translate\n >\n {{ numberOfApps }} apps selected for branding\n </p>\n <ng-template #noapps>\n <p\n class=\"text-center text-medium\"\n translate\n >\n No apps selected for branding\n </p>\n </ng-template>\n </c8y-li>\n <c8y-li *ngFor=\"let app of brandableApps\">\n <c8y-li-icon>\n <c8y-app-icon\n class=\"icon-40\"\n [app]=\"app\"\n ></c8y-app-icon>\n </c8y-li-icon>\n <c8y-li-checkbox\n [attr.data-cy]=\"'branding-apply-branding-to-app-checkbox-' + app.contextPath\"\n [formControlName]=\"app.contextPath\"\n ></c8y-li-checkbox>\n <c8y-li-body class=\"p-t-8 d-block\">{{ app | humanizeAppName | async }}</c8y-li-body>\n </c8y-li>\n </c8y-list-group>\n</c8y-modal>\n","import { AsyncPipe, NgForOf, NgIf } from '@angular/common';\nimport { Component } from '@angular/core';\nimport { IApplication } from '@c8y/client';\nimport { AppStateService, CellRendererContext, HumanizeAppNamePipe } from '@c8y/ngx-components';\nimport { Observable, of } from 'rxjs';\nimport { map, startWith } from 'rxjs/operators';\n\n@Component({\n selector: 'c8y-branding-tags-cell-renderer',\n templateUrl: './branding-tags-cell-renderer.component.html',\n standalone: true,\n imports: [NgIf, NgForOf, HumanizeAppNamePipe, AsyncPipe]\n})\nexport class BrandingTagsCellRendererComponent {\n hasLatestTag: boolean;\n appContextPathsOrApps$: Observable<Array<string | IApplication>>;\n constructor(\n context: CellRendererContext,\n private appState: AppStateService\n ) {\n const tags: string[] = context.value || [];\n this.hasLatestTag = tags.some(tag => tag === 'latest');\n const appSpecificTags = tags\n .filter(tag => tag.startsWith('app-'))\n .map(tag => tag.replace('app-', ''));\n\n this.appContextPathsOrApps$ = this.getAppContextPathsOrApps$(appSpecificTags);\n }\n\n private getAppContextPathsOrApps$(\n appContextPaths: string[]\n ): Observable<Array<string | IApplication>> {\n if (!appContextPaths.length) {\n return of([]);\n }\n return this.appState.currentAppsOfUser.pipe(\n map(apps => {\n return appContextPaths.map(tag => {\n const app = apps.find(app => app.contextPath === tag);\n return app || tag;\n });\n }),\n startWith(appContextPaths)\n );\n }\n}\n","<div class=\"d-flex a-i-center gap-4 flex-wrap\">\n <div\n class=\"tag tag--success\"\n *ngIf=\"hasLatestTag\"\n translate\n >\n Global\n </div>\n <div\n class=\"tag tag--info\"\n *ngFor=\"let tag of appContextPathsOrApps$ | async\"\n [attr.data-cy]=\"'branding-tags-cell-renderer--tag-' + (tag.contextPath ? tag.contextPath : tag)\"\n >\n {{ tag | humanizeAppName | async }}\n </div>\n</div>\n","import { Component, ViewChild } from '@angular/core';\nimport { CoreModule, DroppedFile, ModalComponent, ZipService } from '@c8y/ngx-components';\nimport { AddBrandingModalService } from '@c8y/ngx-components/branding/shared/lazy/add-branding-modal';\nimport { firstValueFrom } from 'rxjs';\nimport { StaticAsset, StaticAssetsService } from '@c8y/ngx-components/static-assets/data';\nimport { StoreBrandingService } from '@c8y/ngx-components/branding/shared/data';\n\n@Component({\n selector: 'c8y-branding-import-modal',\n templateUrl: './branding-import-modal.component.html',\n standalone: true,\n imports: [CoreModule]\n})\nexport class BrandingImportModalComponent {\n @ViewChild(ModalComponent, { static: true }) modal: ModalComponent;\n\n result = new Promise<void>((resolve, reject) => {\n this._resovle = resolve;\n this._reject = reject;\n });\n files: DroppedFile[] = [];\n loading = false;\n\n private _resovle: () => void;\n private _reject: (reason?: any) => void;\n\n constructor(\n private addBrandingModalService: AddBrandingModalService,\n private zip: ZipService,\n private staticAssets: StaticAssetsService,\n private brandings: StoreBrandingService\n ) {}\n\n async droppedFile(event: DroppedFile[]) {\n this.loading = true;\n try {\n this.files = event;\n await this.import(event[0].file);\n } catch (e) {\n console.error(e);\n this.files = [];\n this.loading = false;\n return;\n }\n this.modal._close();\n this._resovle();\n }\n\n async import(file: File) {\n const versionDetails = await this.addBrandingModalService.openAddBrandingModal();\n if (!versionDetails) {\n throw Error('No version details provided');\n }\n\n // verify if branding is already present\n const { variants } = await this.brandings.loadBrandingVariants();\n if (!variants?.length) {\n await this.brandings.getStartedUsingBranding();\n }\n\n const importedZipEntries = await firstValueFrom(this.zip.getEntries(file));\n const staticAssetsEntry = importedZipEntries.find(\n entry => entry.filename === this.staticAssets.fileNames.exportZipName\n );\n if (!staticAssetsEntry) {\n throw Error(\n `Missing \"${this.staticAssets.fileNames.exportZipName}\" file in the uploaded zip.`\n );\n }\n const staticAssetsZip = await firstValueFrom(this.zip.getData(staticAssetsEntry));\n const staticAssetsZipFile = new File([staticAssetsZip], staticAssetsEntry.filename);\n const staticAssetsZipEntries = await firstValueFrom(this.zip.getEntries(staticAssetsZipFile));\n const filesToAdd = new Array<File>();\n let oldAssets: Array<StaticAsset>;\n let newAssets: Array<StaticAsset>;\n\n const contentsFileEntry = staticAssetsZipEntries.find(\n entry => entry.filename === this.staticAssets.fileNames.contents\n );\n if (contentsFileEntry) {\n const fileContent = await (await firstValueFrom(this.zip.getData(contentsFileEntry))).text();\n oldAssets = JSON.parse(fileContent);\n }\n for (const entry of staticAssetsZipEntries) {\n if (\n entry.filename === this.brandings.fileNames.manifest ||\n entry.filename === this.staticAssets.fileNames.contents\n ) {\n continue;\n }\n const file = await firstValueFrom(this.zip.getData(entry));\n const metadata = oldAssets?.find(asset => asset.fileName === entry.filename);\n if (metadata) {\n filesToAdd.push(\n new File([file], entry.filename, {\n type: metadata.type,\n lastModified: metadata.lastModified\n })\n );\n } else {\n filesToAdd.push(new File([file], entry.filename));\n }\n }\n if (filesToAdd.length > 0) {\n const currentStaticAssets = await this.staticAssets.listFilesCached('branding', true);\n newAssets = await this.staticAssets.addFilesToStaticAssets(\n 'branding',\n filesToAdd,\n currentStaticAssets,\n false\n );\n }\n\n const publicOptionsEntry = importedZipEntries.find(\n entry => entry.filename === this.brandings.fileNames.exportZipName\n );\n if (!publicOptionsEntry) {\n throw Error(`Missing \"${this.brandings.fileNames.exportZipName}\" file in the uploaded zip.`);\n }\n const publicOptionsZip = await firstValueFrom(this.zip.getData(publicOptionsEntry));\n const publicOptionsZipFile = new File([publicOptionsZip], publicOptionsEntry.filename);\n\n const publicOptionsFiles = await firstValueFrom(this.zip.getEntries(publicOptionsZipFile));\n const optionsFileEntry = publicOptionsFiles.find(\n entry => entry.filename === this.brandings.fileNames.options\n );\n if (!optionsFileEntry) {\n throw Error(\n `Missing \"${this.brandings.fileNames.options}\" file in \"${this.brandings.fileNames.exportZipName}\" file of the uploaded zip.`\n );\n }\n\n const optionsFile = await firstValueFrom(this.zip.getData(optionsFileEntry));\n let options = JSON.parse(await optionsFile.text());\n\n if (oldAssets.length && newAssets.length) {\n options = this.brandings.replaceBrandingAssetsInBrandingOptions(\n options,\n oldAssets,\n newAssets\n );\n }\n\n const zip = await this.brandings.getBrandingZip(options);\n\n await this.brandings.saveBranding(zip, `${versionDetails.brandingName}-1`, [\n versionDetails.brandingName\n ]);\n }\n\n cancel() {\n this._reject();\n }\n}\n","<c8y-modal\n [title]=\"'Import branding variant'\"\n [headerClasses]=\"'dialog-header'\"\n (onDismiss)=\"cancel()\"\n [labels]=\"{ cancel: 'Cancel' }\"\n>\n <ng-container c8y-modal-title>\n <span c8yIcon=\"import\"></span>\n </ng-container>\n <div class=\"p-t-16 p-b-16 p-l-24 p-r-24 separator-bottom\">\n <p\n class=\"text-medium text-center text-16\"\n translate\n >\n Easily copy any branding variant from any tenant\n </p>\n <p translate class=\"p-t-8\">\n Export the branding variant from the source tenant and import the .zip file here. This process\n allows you to maintain consistent branding across multiple tenants efficiently.\n </p>\n </div>\n <div\n class=\"p-24 m-r-auto m-l-auto\"\n style=\"max-width: 300px\"\n >\n <c8y-drop-area\n [icon]=\"'upload'\"\n (dropped)=\"droppedFile($event)\"\n [files]=\"files\"\n [accept]=\"'.zip'\"\n [maxAllowedFiles]=\"1\"\n [loading]=\"loading\"\n ></c8y-drop-area>\n </div>\n</c8y-modal>\n","import { Injectable } from '@angular/core';\nimport { BrandingImportModalComponent } from './branding-import-modal.component';\nimport { BsModalService } from 'ngx-bootstrap/modal';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class BrandingImportModalService {\n constructor(private modal: BsModalService) {}\n\n async openBrandingImportModal() {\n let versionDetails: Awaited<BrandingImportModalComponent['result']>;\n try {\n const modalRef = this.modal.show(BrandingImportModalComponent, {\n class: 'modal-sm',\n ignoreBackdropClick: true,\n keyboard: false\n });\n versionDetails = await modalRef.content.result;\n } catch (e) {\n // modal closed\n return;\n }\n\n return versionDetails;\n }\n}\n","import { Component } from '@angular/core';\nimport { Router, RouterLink } from '@angular/router';\nimport { CellRendererContext } from '@c8y/ngx-components';\n\n@Component({\n selector: 'c8y-branding-name-cell-renderer',\n templateUrl: './branding-name-cell-renderer.component.html',\n standalone: true,\n imports: [RouterLink]\n})\nexport class BrandingNameCellRendererComponent {\n name: string;\n routerLink: string;\n constructor(\n context: CellRendererContext,\n private router: Router\n ) {\n this.name = context.value;\n this.routerLink = `${this.router.url}/${context.item.name}`;\n }\n}\n","<a [routerLink]=\"routerLink\">{{ name }}</a>\n","import { Component } from '@angular/core';\nimport { C8yTranslatePipe } from '@c8y/ngx-components';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { PopoverModule } from 'ngx-bootstrap/popover';\n\n@Component({\n selector: 'c8y-branding-tags-header-cell-renderer',\n templateUrl: './branding-tags-header-cell-renderer.component.html',\n standalone: true,\n imports: [PopoverModule, C8yTranslatePipe]\n})\nexport class BrandingTagsHeaderCellRendererComponent {\n readonly heading = gettext('Applied to');\n readonly popoverContent = gettext(`<p class=\"m-b-8\">Branding can be applied at two levels</p>\n <ul class=\"list-unstyled m-b-0\">\n <li class=\"m-b-4\">\n <strong>Global: </strong>\n <span>Applied to the entire platform, affecting all apps and interfaces.</span>\n </li>\n <li>\n <strong>App-specific: </strong>\n <span>Applied only to selected apps within the platform.</span>\n </li>\n </ul>`);\n}\n","<div class=\"d-flex\">\n <span\n class=\"text-truncate\"\n [title]=\"heading | translate\"\n >\n {{ heading | translate }}\n </span>\n <button\n class=\"btn-help btn-help--sm a-s-center\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"applyBrandingPopover\"\n placement=\"bottom\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n (click)=\"$event.stopPropagation()\"\n >\n <i c8yIcon=\"question-circle-o\"></i>\n </button>\n <ng-template #applyBrandingPopover>\n <div [innerHTML]=\"popoverContent | translate\"></div>\n </ng-template>\n</div>\n","import { Component } from '@angular/core';\nimport {\n BrandVersion,\n StoreBrandingService,\n BrandingVersionService,\n BrandingTrackingService\n} from '@c8y/ngx-components/branding/shared/data';\nimport { BehaviorSubject, Observable, firstValueFrom } from 'rxjs';\nimport { map, shareReplay, switchMap } from 'rxjs/operators';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport {\n ActionControl,\n AlertService,\n AppStateService,\n BuiltInActionType,\n Column,\n DisplayOptions,\n ModalService,\n Pagination,\n Status,\n ZipService,\n DataGridComponent,\n BreadcrumbComponent,\n BreadcrumbItemComponent,\n TitleComponent,\n HelpComponent,\n ActionBarItemComponent,\n IconDirective,\n C8yTranslateDirective,\n C8yTranslatePipe,\n EmptyStateComponent,\n LoadingComponent\n} from '@c8y/ngx-components';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { ApplicationService, IApplication } from '@c8y/client';\nimport { saveAs } from 'file-saver';\nimport { StaticAssetsService } from '@c8y/ngx-components/static-assets/data';\nimport { BsModalService } from 'ngx-bootstrap/modal';\nimport { ApplyBrandingToAppModalComponent } from '../apply-branding-to-app-modal/apply-branding-to-app-modal.component';\nimport { AddBrandingModalService } from '@c8y/ngx-components/branding/shared/lazy/add-branding-modal';\nimport { BrandingTagsCellRendererComponent } from '../branding-tags-cell-renderer/branding-tags-cell-renderer.component';\nimport { BrandingImportModalService } from '../branding-import-modal/branding-import-modal.service';\nimport { BrandingNameCellRendererComponent } from '../branding-name-cell-renderer/branding-name-cell-renderer.component';\nimport { BrandingTagsHeaderCellRendererComponent } from '../branding-tags-header-cell-renderer/branding-tags-header-cell-renderer.component';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'c8y-branding',\n templateUrl: './branding.component.html',\n standalone: true,\n imports: [\n BreadcrumbComponent,\n BreadcrumbItemComponent,\n TitleComponent,\n HelpComponent,\n ActionBarItemComponent,\n IconDirective,\n C8yTranslateDirective,\n C8yTranslatePipe,\n EmptyStateComponent,\n LoadingComponent,\n DataGridComponent,\n AsyncPipe\n ]\n})\nexport class BrandingComponent {\n pagination: Pagination = {\n pageSize: 10,\n currentPage: 1\n };\n displayOptions: DisplayOptions = {\n bordered: false,\n striped: true,\n filter: true,\n gridHeader: true,\n hover: true\n };\n actionControls: ActionControl[] = [\n {\n type: BuiltInActionType.Edit,\n callback: (entry: BrandVersion, _reload) => {\n this.editBranding(entry.name);\n }\n },\n {\n type: BuiltInActionType.Export,\n callback: async (entry: BrandVersion, _reload) => {\n await this.exportBranding(entry);\n }\n },\n {\n type: 'preview',\n icon: 'external-link',\n text: gettext('Open preview'),\n callback: async (entry: BrandVersion, _reload) => {\n this.brandings.openPreviewForBranding(entry.name);\n }\n },\n {\n type: 'duplicate',\n icon: 'copy',\n text: gettext('Duplicate'),\n callback: async (entry: BrandVersion, _reload) => {\n await this.duplicateVersion(entry);\n this.refresh();\n }\n },\n {\n type: 'apply-to-app',\n icon: 'form',\n text: gettext('Apply to specific apps'),\n callback: async (entry: BrandVersion, _reload) => {\n await this.applyToApps(entry);\n this.refresh();\n }\n },\n {\n type: 'set-as-default',\n icon: 'globe1',\n text: gettext('Set as global'),\n showIf: (entry: BrandVersion) => {\n if (entry.tags?.includes('latest')) {\n return false;\n }\n return true;\n },\n callback: async (entry: BrandVersion, _reload) => {\n try {\n await this.confirmModal.confirm(\n gettext('Set as global branding'),\n gettext(\n 'Are you sure that you want to set this variant as the global branding? By doing so, this variant will be applied to all apps that do not have a specific branding applied. Do you want to proceed?'\n ),\n Status.INFO\n );\n } catch {\n // did not confirm\n return;\n }\n await this.brandings.markAsActive(entry);\n this.refresh();\n }\n },\n {\n type: BuiltInActionType.Delete,\n showIf: (entry: BrandVersion) => {\n if (entry.tags?.includes('latest')) {\n return false;\n }\n return true;\n },\n callback: async (entry: BrandVersion, _reload) => {\n try {\n await this.confirmModal.confirm(\n gettext('Delete branding variant'),\n gettext(\n 'You are about to delete this branding variant. This action cannot be undone. Do you want to proceed?'\n ),\n Status.DANGER\n );\n } catch {\n // did not confirm\n return;\n }\n\n this.brandingTracking.deleteBrandingVariant();\n await this.applicationService.deleteVersionPackage(entry.publicOptionsApp, {\n version: entry.version\n });\n this.refresh();\n }\n }\n ];\n columns: Column[] = [\n {\n name: 'name',\n header: gettext('Name'),\n path: 'name',\n filterable: true,\n cellRendererComponent: BrandingNameCellRendererComponent\n },\n {\n name: gettext('Applied to`Applications a branding applies to (table column header)`'),\n path: 'tags',\n filterable: false,\n cellCSSClassName: 'small',\n cellRendererComponent: BrandingTagsCellRendererComponent,\n headerCellRendererComponent: BrandingTagsHeaderCellRendererComponent\n },\n {\n name: 'owner',\n header: gettext('Owner'),\n path: 'owner',\n filterable: false\n },\n {\n name: 'lastUpdated',\n header: gettext('Last updated'),\n path: 'lastUpdated',\n filterable: false\n }\n ];\n currentAppContextPath = 'administration';\n availableBrandingVariants$: Observable<{\n publicOptions: IApplication;\n variants: BrandVersion[];\n }>;\n\n private reloadTrigger = new BehaviorSubject<void>(undefined);\n constructor(\n private brandings: StoreBrandingService,\n private activatedRoute: ActivatedRoute,\n private appState: AppStateService,\n private applicationService: ApplicationService,\n private zip: ZipService,\n private staticAssets: StaticAssetsService,\n private router: Router,\n private modal: BsModalService,\n private confirmModal: ModalService,\n private brandingVersionService: BrandingVersionService,\n private addBrandingModalService: AddBrandingModalService,\n private alert: AlertService,\n private brandingImportModalService: BrandingImportModalService,\n private brandingTracking: BrandingTrackingService\n ) {\n this.currentAppContextPath = this.appState.state.app?.contextPath || this.currentAppContextPath;\n this.availableBrandingVariants$ = this.reloadTrigger.pipe(\n switchMap(() => this.brandings.loadBrandingVariants()),\n // hide the fallback from users\n map(variants => {\n variants.variants = variants.variants.filter(v => !v.tags?.includes('fallback'));\n return variants;\n }),\n shareReplay({ refCount: true, bufferSize: 1 })\n );\n }\n\n async deleteAllBrandings(publicOptions?: IApplication) {\n const result = await this.confirmModal.confirm(\n gettext('Delete all branding variants'),\n gettext(\n `<p class=\"m-b-8\">\n This action will permanently remove all custom branding variants. Your tenant will revert\n to the default branding.\n </p>\n <p class=\"m-b-8\">\n Consider exporting your custom branding variants before proceeding, as this action cannot\n be undone.\n </p>\n <p>\n Are you sure you want to continue?\n </p>`\n ),\n Status.DANGER,\n { cancel: gettext('Cancel'), ok: gettext('Delete all') }\n );\n if (result === false || (typeof result === 'object' && !result.confirmed)) {\n return;\n }\n\n this.brandingTracking.deleteAllBrandings();\n\n await this.brandings.deleteAllBrandings(publicOptions);\n this.refresh();\n }\n\n async exportBranding(variant: BrandVersion) {\n this.brandingTracking.exportBranding();\n const branding = await firstValueFrom(this.brandings.getZipForBinary(variant.id));\n const staticAssetsApp = await this.staticAssets.getAppForTenant('branding');\n const staticAssetsZip = await firstValueFrom(\n this.brandings.getZipForBinary(\n staticAssetsApp.activeVersionId,\n this.staticAssets.fileNames.exportZipName\n )\n );\n const blob = await this.zip.createZip([\n { fileName: this.brandings.fileNames.exportZipName, blob: branding },\n { fileName: this.staticAssets.fileNames.exportZipName, blob: staticAssetsZip }\n ]);\n saveAs(blob, `branding-export-${variant.name}-${variant.revision}-${new Date().getTime()}.zip`);\n }\n\n refresh() {\n this.reloadTrigger.next();\n }\n\n async addNewVersion() {\n const versionDetails = await this.addBrandingModalService.openAddBrandingModal();\n if (!versionDetails) {\n this.refresh();\n return;\n }\n let fallBackBranding = {};\n try {\n fallBackBranding = await this.brandings.getBrandingOptionsForVersion('latest');\n } catch (e) {\n console.warn(`Failed to load latest branding`);\n }\n\n this.brandingTracking.addNewVersion();\n\n try {\n await this.brandings.addBranding(\n this.brandingVersionService.createInitialBrandingVersion(versionDetails.brandingName),\n fallBackBranding,\n [versionDetails.brandingName]\n );\n await this.brandings.waitForBrandingToBePresent(versionDetails.brandingName);\n this.editBranding(versionDetails.brandingName);\n } catch (e) {\n this.alert.addServerFailure(e, 'danger');\n }\n }\n\n async duplicateVersion(version: BrandVersion) {\n const options = await this.brandings.getBrandingOptionsForVersion(version.version);\n const versionDetails = await this.addBrandingModalService.openDuplicateBrandingModal();\n if (!versionDetails) {\n return;\n }\n\n this.brandingTracking.duplicateVersion();\n\n try {\n await this.brandings.addBranding(\n this.brandingVersionService.createInitialBrandingVersion(versionDetails.brandingName),\n options,\n [versionDetails.brandingName]\n );\n await this.brandings.waitForBrandingToBePresent(versionDetails.brandingName);\n this.editBranding(versionDetails.brandingName);\n } catch (e) {\n this.alert.addServerFailure(e, 'danger');\n }\n }\n\n async editBranding(brandingName: string) {\n return this.router.navigate([brandingName, 'edit'], { relativeTo: this.activatedRoute });\n }\n\n async getStartedUsingBranding() {\n const result = await this.confirmModal.confirm(\n gettext(`Get started using branding variants`),\n gettext(\n `<p class=\"m-b-8\">Confirming this action creates two branding variants:</p>\n <ul class=\"m-b-8 p-l-16\">\n <li>A default copy of your current global branding</li>\n <li>Your new customizable variant.</li>\n </ul>\n <p class=\"m-b-8\">\n After customization, you can set your new variant as global or apply it to specific apps.\n </p>\n <p>\n To revert changes, simply set the default branding as global or use the \"Delete all variants\" button at any time.\n </p>`\n )\n );\n if (result === false || (typeof result === 'object' && !result.confirmed)) {\n return;\n }\n\n this.brandingTracking.getStartedUsingBranding();\n\n await this.brandings.getStartedUsingBranding();\n await this.addNewVersion();\n }\n\n async applyToApps(version: BrandVersion) {\n const prefix = 'app-';\n const currentTags = version.tags\n .filter(tag => tag.startsWith(prefix))\n .map(tag => tag.replace(prefix, ''));\n const otherTags = version.tags.filter(tag => !tag.startsWith(prefix));\n let selectedTags: Awaited<ApplyBrandingToAppModalComponent['result']>;\n try {\n const modalRef = this.modal.show(ApplyBrandingToAppModalComponent, {\n initialState: { currentTags },\n ignoreBackdropClick: true,\n keyboard: false\n });\n selectedTags = await modalRef.content.result;\n } catch (e) {\n // modal closed\n return;\n }\n\n const selectedApps = Object.keys(selectedTags).filter(key => selectedTags[key]);\n\n this.brandingTracking.applyToApps(selectedApps);\n\n const tagsToSet = [...otherTags, ...selectedApps.map(tag => `${prefix}${tag}`)];\n\n await this.removeTagsFromOtherVersions(version.publicOptionsApp, tagsToSet, version.version);\n await this.applicationService.setPackageVersionTag(\n version.publicOptionsApp,\n version.version,\n tagsToSet\n );\n }\n\n async importBranding() {\n try {\n await this.brandingImportModalService.openBrandingImportModal();\n\n this.brandings.refreshTriggerBrandingVariants.next();\n this.reloadTrigger.next();\n } catch (e) {\n // modal closed\n }\n }\n\n private async removeTagsFromOtherVersions(\n publicOptionsApp: IApplication,\n tagsToRemove: string[],\n versionToIgnore?: string\n ) {\n for (const version of publicOptionsApp.applicationVersions) {\n if (version.version === versionToIgnore) {\n continue;\n }\n const removedTags = version.tags.filter(tag => !tagsToRemove.includes(tag));\n if (removedTags.length === version.tags.length) {\n continue;\n }\n await this.applicationService.setPackageVersionTag(\n publicOptionsApp,\n version.version,\n removedTags\n );\n }\n }\n}\n","<c8y-title>{{ 'Branding' | translate }}</c8y-title>\n\n<c8y-breadcrumb>\n <c8y-breadcrumb-item\n [icon]=\"'cog'\"\n [label]=\"'Settings' | translate\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n [icon]=\"'palette'\"\n [label]=\"'Branding' | translate\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n [icon]=\"'palette'\"\n [label]=\"'Variants' | translate\"\n [path]=\"'/branding-editor/variants'\"\n ></c8y-breadcrumb-item>\n</c8y-breadcrumb>\n\n@let brandingVariant = availableBrandingVariants$ | async;\n@if (!brandingVariant) {\n <c8y-loading></c8y-loading>\n} @else {\n @if (brandingVariant.publicOptions) {\n <c8y-action-bar-item\n [placement]=\"'right'\"\n [priority]=\"30\"\n >\n <button\n class=\"btn btn-link\"\n (click)=\"addNewVersion()\"\n data-cy=\"branding-add-branding-variant\"\n >\n <i c8yIcon=\"plus-circle\"></i>\n <span translate>Add variant</span>\n </button>\n </c8y-action-bar-item>\n }\n <c8y-action-bar-item\n [placement]=\"'right'\"\n [priority]=\"20\"\n >\n <button\n class=\"btn btn-link\"\n data-cy=\"branding-import-branding\"\n (click)=\"importBranding()\"\n >\n <i [c8yIcon]=\"'data-import'\"></i>\n <span translate>Import variant</span>\n </button>\n </c8y-action-bar-item>\n\n @if (brandingVariant.publicOptions) {\n <c8y-action-bar-item\n [placement]=\"'right'\"\n [priority]=\"10\"\n >\n <button\n class=\"btn btn-link\"\n (click)=\"deleteAllBrandings(brandingVariant.publicOptions)\"\n data-cy=\"branding-remove-all-brandings\"\n >\n <i [c8yIcon]=\"'trash-o'\"></i>\n <span translate>Delete all variants</span>\n </button>\n </c8y-action-bar-item>\n }\n\n <c8y-help\n [src]=\"'/docs/enterprise-tenant/customization/#to-configure-branding-settings'\"\n ></c8y-help>\n\n <div class=\"content-fullpage d-flex d-col border-top\">\n <c8y-data-grid\n [title]=\"'Branding variants' | translate\"\n [columns]=\"columns\"\n [actionControls]=\"actionControls\"\n [pagination]=\"pagination\"\n [displayOptions]=\"displayOptions\"\n (onReload)=\"refresh()\"\n [rows]=\"brandingVariant.variants\"\n >\n @if (!brandingVariant.publicOptions) {\n <c8y-ui-empty-state\n [icon]=\"'palette'\"\n [title]=\"'No branding variants' | translate\"\n [subtitle]=\"\n 'Personalize your experience with a theme-able interface. Create a unique look that aligns with your brand identity.'\n | translate\n \"\n [horizontal]=\"false\"\n >\n <button\n class=\"btn btn-default\"\n data-cy=\"branding-get-started-using-branding\"\n (click)=\"getStartedUsingBranding()\"\n translate\n >\n Add branding variant\n </button>\n </c8y-ui-empty-state>\n }\n </c8y-data-grid>\n </div>\n}\n","import { Component } from '@angular/core';\nimport { ControlValueAccessor, FormBuilder, FormsModule } from '@angular/forms';\nimport { CollapseModule } from 'ngx-bootstrap/collapse';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { CoreModule, MessageBannerService } from '@c8y/ngx-components';\nimport { cloneDeep } from 'lodash-es';\nimport { StaticAssetsFilePickerComponent } from '@c8y/ngx-components/static-assets';\nimport {\n brandingFormGroupTopLevelEntries,\n brandingFormGroupTopLevelEntriesToUnpack,\n BrandingOptionsJson,\n createGenericBrandingForm,\n TopLevelValues\n} from '@c8y/ngx-components/branding/shared/data';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { EditorComponent } from '@c8y/ngx-components/editor';\nimport { combineLatest } from 'rxjs';\nimport { debounceTime, distinctUntilChanged } from 'rxjs/operators';\n\n@Component({\n selector: 'c8y-branding-form',\n templateUrl: './branding-form.component.html',\n standalone: true,\n host: { class: 'd-contents' },\n imports: [\n CoreModule,\n FormsModule,\n CollapseModule,\n StaticAssetsFilePickerComponent,\n EditorComponent\n ]\n})\nexport class BrandingFormComponent implements ControlValueAccessor {\n formGroup: ReturnType<BrandingFormComponent['initForm']>;\n messageBannerHelp = gettext(\n 'Supports markdown. Use {{ headerMark }} for headers, {{ listMark }} for lists, {{ boldMark }} for bold, {{ italicMark }} for italic, and {{ linkMark }} for links.'\n );\n messageBannerHelpParams = {\n headerMark: '`#`',\n listMark: '`*`',\n boldMark: '`**`',\n italicMark: '`_`',\n linkMark: '`[]()`'\n };\n private onTouched: () => void;\n\n private externalValue: BrandingOptionsJson = {};\n private formGroupValueChanges$: BrandingFormComponent['formGroup']['valueChanges'];\n\n constructor(\n private formBuilder: FormBuilder,\n private messageBannerService: MessageBannerService\n ) {\n this.formGroup = this.initForm();\n this.formGroupValueChanges$ = this.formGroup.valueChanges.pipe(takeUntilDestroyed());\n this.setupMessageBannerIdAutoGeneration();\n }\n\n writeValue(obj: string): void {\n try {\n const parsedObj = JSON.parse(obj);\n this.externalValue = parsedObj;\n this.applyStateToForm(parsedObj);\n } catch (e) {\n // failed to parse\n }\n }\n\n registerOnChange(fn: (value: string) => void): void {\n this.formGroupValueChanges$.subscribe(value => fn(this.convertFormToState(value)));\n }\n\n registerOnTouched(fn: () => void): void {\n this.onTouched = fn;\n }\n\n cockieBannerChange(toggleState: boolean) {\n this.formGroup.controls.cookieBanner.patchValue({ cookieBannerDisabled: !toggleState });\n }\n\n onBlur(): void {\n this.onTouched();\n }\n\n initForm() {\n return createGenericBrandingForm(this.formBuilder);\n }\n\n applyStateToForm(branding: BrandingOptionsJson) {\n if (!branding) {\n return;\n }\n let toBePatched = brandingFormGroupTopLevelEntries.reduceRight((prev, curr) => {\n if (brandingFormGroupTopLevelEntriesToUnpack.includes(curr as any)) {\n return Object.assign(prev, { [curr]: branding });\n }\n return Object.assign(prev, { [curr]: branding[curr] });\n }, {});\n\n toBePatched = Object.keys(this.formGroup.controls)\n .filter(key => !brandingFormGroupTopLevelEntries.includes(key as any))\n .reduceRight((prev, curr) => {\n return Object.assign(prev, { [curr]: branding.brandingCssVars || {} });\n }, toBePatched);\n this.formGroup.patchValue(toBePatched);\n\n this.ensureMessageBannerIdExists();\n }\n\n convertFormToState(value: BrandingFormComponent['formGroup']['value']): string {\n const topLevel: TopLevelValues = brandingFormGroupTopLevelEntries.reduceRight((prev, curr) => {\n if (brandingFormGroupTopLevelEntriesToUnpack.includes(curr as any)) {\n return Object.assign(prev, value[curr]);\n }\n return Object.assign(prev, { [curr]: value[curr] });\n }, {});\n const newBrandingCssVars = Object.entries(value).reduceRight((prev, [key, value]) => {\n if (brandingFormGroupTopLevelEntries.includes(key as any)) {\n return prev;\n }\n return Object.assign(prev, value);\n }, {});\n const externalValue: BrandingOptionsJson = cloneDeep(this.externalValue || {});\n Object.assign(externalValue, topLevel);\n const currentCSSVars = externalValue['brandingCssVars'] || {};\n const assignedNewValues = Object.assign(currentCSSVars, newBrandingCssVars);\n externalValue['brandingCssVars'] = assignedNewValues;\n return JSON.stringify(externalValue, null, 2);\n }\n\n previewBanner(): void {\n this.messageBannerService.showBanner(true);\n }\n\n private setupMessageBannerIdAutoGeneration(): void {\n const messageBannerGroup = this.formGroup.controls.messageBanner;\n\n combineLatest([\n messageBannerGroup.controls.messageBannerContent.valueChanges,\n messageBannerGroup.controls.messageBannerType.valueChanges,\n messageBannerGroup.controls.messageBannerEnabled.valueChanges\n ])\n .pipe(\n debounceTime(300),\n distinctUntilChanged((prev, curr) => {\n return JSON.stringify(prev) === JSON.stringify(curr);\n }),\n takeUntilDestroyed()\n )\n .subscribe(() => {\n messageBannerGroup.controls.messageBannerId.setValue(crypto.randomUUID());\n });\n }\n\n private async ensureMessageBannerIdExists(): Promise<void> {\n const messageBannerGroup = this.formGroup.controls.messageBanner;\n const content = messageBannerGroup.controls.messageBannerContent.value;\n const currentId = messageBannerGroup.controls.messageBannerId.value;\n\n if (content && !currentId) {\n messageBannerGroup.controls.messageBannerId.setValue(crypto.randomUUID());\n }\n }\n}\n","<c8y-help [src]=\"'/docs/enterprise-tenant/customization/#generic-tab'\"></c8y-help>\n\n<div\n class=\"inner-scroll\"\n [formGroup]=\"formGroup\"\n>\n <!-- Title & Favicon-->\n <div class=\"card-block\">\n <div class=\"row\">\n <div class=\"col-xs-12 col-sm-3 col-md-2 p-t-16\">\n <h4 class=\"text-normal text-right text-left-xs m-b-16\">\n {{ 'Title & favicon' | translate }}\n </h4>\n </div>\n <div class=\"col-xs-12 col-sm-9 col-md-10\">\n <div\n class=\"row p-t-16 p-b-16\"\n [formGroupName]=\"'genericApplicationOptions'\"\n >\n <div class=\"col-md-6\">\n <c8y-form-group class=\"m-b-32\">\n <label\n for=\"globalTitle\"\n translate\n >\n Title\n </label>\n <input\n class=\"form-control\"\n id=\"globalTitle\"\n name=\"globalTitle\"\n type=\"text\"\n formControlName=\"globalTitle\"\n />\n <c8y-messages\n [helpMessage]=\"'Title to display in the browser tab' | translate\"\n ></c8y-messages>\n </c8y-form-group>\n </div>\n <div class=\"col-md-6\">\n <c8y-form-group>\n <label translate>Favicon</label>\n <c8y-static-assets-file-picker\n name=\"faviconUrl\"\n [supportedFileExtensions]=\"['ico']\"\n [isCSSURL]=\"false\"\n formControlName=\"faviconUrl\"\n ></c8y-static-assets-file-picker>\n <c8y-messages [helpMessage]=\"'Supported files: *.ico' | translate\"></c8y-messages>\n </c8y-form-group>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"card-block separator-top\">\n <div class=\"col-xs-12 col-sm-3 col-md-2 p-t-16\">\n <h4\n class=\"text-normal text-right text-left-xs m-b-16\"\n translate\n >\n Dark theme\n </h4>\n </div>\n <div class=\"col-xs-12 col-sm-9 col-md-10\">\n <c8y-form-group class=\"m-b-32 p-t-8 m-t-4\">\n <label class=\"c8y-switch\">\n <input\n class=\"form-control\"\n name=\"darkThemeAvailable\"\n type=\"checkbox\"\n formControlName=\"darkThemeAvailable\"\n />\n <span></span>\n <span translate>Enable dark theme support</span>\n </label>\n </c8y-form-group>\n </div>\n </div>\n <!-- Typography -->\n <div class=\"card-block separator-top\">\n <div class=\"row\">\n <div class=\"col-xs-12 col-sm-3 col-md-2 p-t-16\">\n <h4\n class=\"text-normal text-right text-left-xs m-b-16\"\n translate\n >\n Typography\n </h4>\n </div>\n <div class=\"col-xs-12 col-sm-9 col-md-10\">\n <div class=\"row\">\n <div class=\"col-md-6\">\n <fieldset\n class=\"c8y-fieldset p-24\"\n [formGroupName]=\"'baseTypography'\"\n >\n <legend translate>Base typography</legend>\n <c8y-form-group class=\"m-b-32\">\n <label\n for=\"font-url\"\n translate\n >\n Fonts URL\n </label>\n <input\n class=\"form-control\"\n id=\"font-url\"\n placeholder=\"{{\n 'e.g. {{ example }}' | translate : { example: 'https://fonts.googleapis.com/css?family=Roboto:400,500,700' }\n }}\"\n type=\"text\"\n formControlName=\"font-url\"\n />\n <c8y-messages [helpMessage]=\"'The Google fonts URL' | translate\"></c8y-messages>\n </c8y-form-group>\n <c8y-form-group class=\"m-b-32\">\n <label\n title=\"font-family-base\"\n for=\"font-family-base\"\n translate\n >\n Base font stack\n </label>\n <input\n class=\"form-control\"\n id=\"font-family-base\"\n placeholder=\"{{ 'e.g. {{ example }}' | translate : {example: '&quot;Roboto&quot;, Arial, sans-serif'} }}\"\n name=\"font-family-base\"\n type=\"text\"\n formControlName=\"font-family-base\"\n />\n <c8y-messages></c8y-messages>\n </c8y-form-group>\n </fieldset>\n </div>\n <div class=\"col-md-6\">\n <fieldset\n class=\"c8y-fieldset p-24\"\n [formGroupName]=\"'headingsAndNavigatorTypography'\"\n >\n <legend>{{ 'Headings & navigator' | translate }}</legend>\n <c8y-form-group class=\"m-b-32\">\n <label\n title=\"font-family-headings\"\n for=\"font-family-headings\"\n translate\n >\n Headings font stack\n </label>\n <input\n class=\"form-control\"\n id=\"font-family-headings\"\n placeholder=\"{{ 'e.g. {{ example }}' | translate : {example: '&quot;Roboto&quot;, Arial, sans-serif'} }}\"\n type=\"text\"\n formControlName=\"font-family-headings\"\n />\n <c8y-messages\n [helpMessage]=\"\n 'Leave empty to use the same font as the base font stack' | translate\n \"\n ></c8y-messages>\n </c8y-form-group>\n <c8y-form-group class=\"m-b-32\">\n <label\n title=\"navigator-font-family\"\n translate\n >\n Navigator font stack\n </label>\n <div class=\"form-control-static\">\n <label class=\"c8y-radio radio-inline\">\n <input\n name=\"navigator-font-family\"\n type=\"radio\"\n formControlName=\"navigator-font-family\"\n [value]=\"'var(--font-family-base)'\"\n />\n <span></span>\n <span translate>Match base font stack</span>\n </label>\n <label\n class=\"c8y-radio radio-inline\"\n title=\"Radio Two\"\n >\n <input\n name=\"navigator-font-family\"\n type=\"radio\"\n formControlName=\"navigator-font-family\"\n [value]=\"'var(--font-family-headings)'\"\n />\n <span></span>\n <span translate>Match headings font stack</span>\n </label>\n </div>\n </c8y-form-group>\n </fieldset>\n </div>\n </div>\n </div>\n </div>\n </div>\n <!-- Cookie banner -->\n <div class=\"card-block separator-top\">\n <div class=\"row\">\n <div class=\"col-xs-12 col-sm-3 col-md-2 p-t-16\">\n <h4\n class=\"text-normal text-right