UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

642 lines (634 loc) 169 kB
import * as i0 from '@angular/core'; import { Injectable, Input, Component, ViewChild } from '@angular/core'; import * as i1$2 from '@c8y/ngx-components/branding/shared/data'; import { createGenericBrandingForm, brandingFormGroupTopLevelEntries, brandingFormGroupTopLevelEntriesToUnpack, createBrandingForm } from '@c8y/ngx-components/branding/shared/data'; import { merge, of, firstValueFrom, BehaviorSubject, defer, interval } from 'rxjs'; import { map, startWith, switchMap, shareReplay, distinctUntilChanged, filter, take, pairwise, delay } from 'rxjs/operators'; import * as i2$1 from '@angular/router'; import { RouterLink, RouterOutlet } from '@angular/router'; import * as i3 from '@c8y/ngx-components'; import { ModalComponent, C8yTranslateDirective, ListItemComponent, ListItemIconComponent, ListItemCheckboxComponent, AppIconComponent, ListItemBodyComponent, C8yTranslatePipe, HumanizeAppNamePipe, CoreModule, BuiltInActionType, Status, BreadcrumbComponent, BreadcrumbItemComponent, TitleComponent, HelpComponent, ActionBarItemComponent, IconDirective, EmptyStateComponent, LoadingComponent, DataGridComponent, GENERIC_FILE_TYPE, ColorInputComponent } from '@c8y/ngx-components'; import { gettext } from '@c8y/ngx-components/gettext'; import * as i1 from '@c8y/client'; import { ApplicationType } from '@c8y/client'; import { saveAs } from 'file-saver'; import * as i3$1 from '@c8y/ngx-components/static-assets/data'; import * as i1$3 from 'ngx-bootstrap/modal'; import * as i2 from '@angular/forms'; import { ReactiveFormsModule, FormsModule, FormControl } from '@angular/forms'; import * as i5 from '@angular/common'; import { NgIf, NgForOf, AsyncPipe } from '@angular/common'; import { uniqBy, cloneDeep } from 'lodash-es'; import * as i1$1 from '@c8y/ngx-components/branding/shared/lazy/add-branding-modal'; import * as i1$4 from 'ngx-bootstrap/popover'; import { PopoverModule } from 'ngx-bootstrap/popover'; import * as i4 from 'ngx-bootstrap/collapse'; import { CollapseModule } from 'ngx-bootstrap/collapse'; import { StaticAssetsFilePickerComponent } from '@c8y/ngx-components/static-assets'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EditorComponent } from '@c8y/ngx-components/editor'; import { StaticAssetsFileListComponent } from '@c8y/ngx-components/static-assets/modal'; import * as i2$2 from '@ngx-translate/core'; class ApplyBrandingToAppService { constructor(apps) { this.apps = apps; } async getBrandableApps() { const { data: apps } = await this.apps.list({ pageSize: 2000, dropOverwrittenApps: true, type: ApplicationType.HOSTED }); const appsWithDynamicOptionsUrl = this.getHostedAppsWhereDynamicOptionsUrlIsUndefined(apps); return appsWithDynamicOptionsUrl; } getHostedAppsWhereDynamicOptionsUrlIsUndefined(apps) { return apps.filter(app => { const manifest = app.manifest; if (!manifest) { return false; } // filter out none web sdk based apps if (!manifest.webSdkVersion) { return false; } // filter out packages if (manifest.isPackage) { return false; } const dynamicOptionsUrl = manifest.dynamicOptionsUrl; return dynamicOptionsUrl === undefined || dynamicOptionsUrl === true; }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ApplyBrandingToAppService, deps: [{ token: i1.ApplicationService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ApplyBrandingToAppService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ApplyBrandingToAppService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.ApplicationService }] }); class ApplyBrandingToAppModalComponent { constructor(apply, formBuilder) { this.apply = apply; this.formBuilder = formBuilder; this.brandableApps = []; this.result = new Promise((resolve, reject) => { this._resovle = resolve; this._reject = reject; }); } async ngOnInit() { const brandableApps = await this.apply.getBrandableApps(); this.brandableApps = uniqBy(brandableApps, 'contextPath'); const tags = this.brandableApps.map(app => app.contextPath); this.form = this.initForm(tags); if (this.currentTags.length) { this.form.patchValue(this.currentTags.reduce((prev, curr) => Object.assign(prev, { [curr]: true }), {})); } this.numberOfSelectedApps$ = merge(this.form.valueChanges, of(this.form.value)).pipe(map(value => Object.values(value).filter(Boolean).length)); } save() { this._resovle(this.form.value); } cancel() { this._reject(); } initForm(tags) { const tagsFormMap = tags.reduceRight((prev, curr) => Object.assign(prev, { [curr]: this.formBuilder.control(false) }), {}); return this.formBuilder.group(tagsFormMap); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ApplyBrandingToAppModalComponent, deps: [{ token: ApplyBrandingToAppService }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: ApplyBrandingToAppModalComponent, isStandalone: true, selector: "c8y-apply-branding-to-app-modal", inputs: { currentTags: "currentTags" }, ngImport: i0, template: "<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", dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: ModalComponent, selector: "c8y-modal", inputs: ["disabled", "close", "dismiss", "title", "body", "customFooter", "headerClasses", "labels"], outputs: ["onDismiss", "onClose"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: ListItemIconComponent, selector: "c8y-list-item-icon, c8y-li-icon", inputs: ["icon", "status"] }, { kind: "component", type: ListItemCheckboxComponent, selector: "c8y-list-item-checkbox, c8y-li-checkbox", inputs: ["selected", "indeterminate", "disabled", "displayAsSwitch"], outputs: ["onSelect"] }, { kind: "component", type: AppIconComponent, selector: "c8y-app-icon", inputs: ["contextPath", "name", "app"] }, { kind: "component", type: ListItemBodyComponent, selector: "c8y-list-item-body, c8y-li-body", inputs: ["body"] }, { kind: "directive", type: NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: HumanizeAppNamePipe, name: "humanizeAppName" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ApplyBrandingToAppModalComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-apply-branding-to-app-modal', standalone: true, imports: [ NgIf, AsyncPipe, ReactiveFormsModule, ModalComponent, C8yTranslatePipe, C8yTranslateDirective, ListItemComponent, ListItemIconComponent, ListItemCheckboxComponent, AppIconComponent, ListItemBodyComponent, HumanizeAppNamePipe, NgForOf ], template: "<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" }] }], ctorParameters: () => [{ type: ApplyBrandingToAppService }, { type: i2.FormBuilder }], propDecorators: { currentTags: [{ type: Input }] } }); class BrandingTagsCellRendererComponent { constructor(context, appState) { this.appState = appState; const tags = context.value || []; this.hasLatestTag = tags.some(tag => tag === 'latest'); const appSpecificTags = tags .filter(tag => tag.startsWith('app-')) .map(tag => tag.replace('app-', '')); this.appContextPathsOrApps$ = this.getAppContextPathsOrApps$(appSpecificTags); } getAppContextPathsOrApps$(appContextPaths) { if (!appContextPaths.length) { return of([]); } return this.appState.currentAppsOfUser.pipe(map(apps => { return appContextPaths.map(tag => { const app = apps.find(app => app.contextPath === tag); return app || tag; }); }), startWith(appContextPaths)); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: BrandingTagsCellRendererComponent, deps: [{ token: i3.CellRendererContext }, { token: i3.AppStateService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: BrandingTagsCellRendererComponent, isStandalone: true, selector: "c8y-branding-tags-cell-renderer", ngImport: i0, template: "<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", dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "pipe", type: HumanizeAppNamePipe, name: "humanizeAppName" }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: BrandingTagsCellRendererComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-branding-tags-cell-renderer', standalone: true, imports: [NgIf, NgForOf, HumanizeAppNamePipe, AsyncPipe], template: "<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" }] }], ctorParameters: () => [{ type: i3.CellRendererContext }, { type: i3.AppStateService }] }); class BrandingImportModalComponent { constructor(addBrandingModalService, zip, staticAssets, brandings) { this.addBrandingModalService = addBrandingModalService; this.zip = zip; this.staticAssets = staticAssets; this.brandings = brandings; this.result = new Promise((resolve, reject) => { this._resovle = resolve; this._reject = reject; }); this.files = []; this.loading = false; } async droppedFile(event) { this.loading = true; try { this.files = event; await this.import(event[0].file); } catch (e) { console.error(e); this.files = []; this.loading = false; return; } this.modal._close(); this._resovle(); } async import(file) { const versionDetails = await this.addBrandingModalService.openAddBrandingModal(); if (!versionDetails) { throw Error('No version details provided'); } // verify if branding is already present const { variants } = await this.brandings.loadBrandingVariants(); if (!variants?.length) { await this.brandings.getStartedUsingBranding(); } const importedZipEntries = await firstValueFrom(this.zip.getEntries(file)); const staticAssetsEntry = importedZipEntries.find(entry => entry.filename === this.staticAssets.fileNames.exportZipName); if (!staticAssetsEntry) { throw Error(`Missing "${this.staticAssets.fileNames.exportZipName}" file in the uploaded zip.`); } const staticAssetsZip = await firstValueFrom(this.zip.getData(staticAssetsEntry)); const staticAssetsZipFile = new File([staticAssetsZip], staticAssetsEntry.filename); const staticAssetsZipEntries = await firstValueFrom(this.zip.getEntries(staticAssetsZipFile)); const filesToAdd = new Array(); let oldAssets; let newAssets; const contentsFileEntry = staticAssetsZipEntries.find(entry => entry.filename === this.staticAssets.fileNames.contents); if (contentsFileEntry) { const fileContent = await (await firstValueFrom(this.zip.getData(contentsFileEntry))).text(); oldAssets = JSON.parse(fileContent); } for (const entry of staticAssetsZipEntries) { if (entry.filename === this.brandings.fileNames.manifest || entry.filename === this.staticAssets.fileNames.contents) { continue; } const file = await firstValueFrom(this.zip.getData(entry)); const metadata = oldAssets?.find(asset => asset.fileName === entry.filename); if (metadata) { filesToAdd.push(new File([file], entry.filename, { type: metadata.type, lastModified: metadata.lastModified })); } else { filesToAdd.push(new File([file], entry.filename)); } } if (filesToAdd.length > 0) { const currentStaticAssets = await this.staticAssets.listFilesCached('branding', true); newAssets = await this.staticAssets.addFilesToStaticAssets('branding', filesToAdd, currentStaticAssets, false); } const publicOptionsEntry = importedZipEntries.find(entry => entry.filename === this.brandings.fileNames.exportZipName); if (!publicOptionsEntry) { throw Error(`Missing "${this.brandings.fileNames.exportZipName}" file in the uploaded zip.`); } const publicOptionsZip = await firstValueFrom(this.zip.getData(publicOptionsEntry)); const publicOptionsZipFile = new File([publicOptionsZip], publicOptionsEntry.filename); const publicOptionsFiles = await firstValueFrom(this.zip.getEntries(publicOptionsZipFile)); const optionsFileEntry = publicOptionsFiles.find(entry => entry.filename === this.brandings.fileNames.options); if (!optionsFileEntry) { throw Error(`Missing "${this.brandings.fileNames.options}" file in "${this.brandings.fileNames.exportZipName}" file of the uploaded zip.`); } const optionsFile = await firstValueFrom(this.zip.getData(optionsFileEntry)); let options = JSON.parse(await optionsFile.text()); if (oldAssets.length && newAssets.length) { options = this.brandings.replaceBrandingAssetsInBrandingOptions(options, oldAssets, newAssets); } const zip = await this.brandings.getBrandingZip(options); await this.brandings.saveBranding(zip, `${versionDetails.brandingName}-1`, [ versionDetails.brandingName ]); } cancel() { this._reject(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: BrandingImportModalComponent, deps: [{ token: i1$1.AddBrandingModalService }, { token: i3.ZipService }, { token: i3$1.StaticAssetsService }, { token: i1$2.StoreBrandingService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: BrandingImportModalComponent, isStandalone: true, selector: "c8y-branding-import-modal", viewQueries: [{ propertyName: "modal", first: true, predicate: ModalComponent, descendants: true, static: true }], ngImport: i0, template: "<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", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i3.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i3.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: i3.DropAreaComponent, selector: "c8y-drop-area", inputs: ["formControl", "title", "message", "icon", "loadingMessage", "forceHideList", "alwaysShow", "clickToOpen", "loading", "progress", "maxAllowedFiles", "files", "maxFileSizeInMegaBytes", "accept"], outputs: ["dropped"] }, { kind: "component", type: i3.ModalComponent, selector: "c8y-modal", inputs: ["disabled", "close", "dismiss", "title", "body", "customFooter", "headerClasses", "labels"], outputs: ["onDismiss", "onClose"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: BrandingImportModalComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-branding-import-modal', standalone: true, imports: [CoreModule], template: "<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" }] }], ctorParameters: () => [{ type: i1$1.AddBrandingModalService }, { type: i3.ZipService }, { type: i3$1.StaticAssetsService }, { type: i1$2.StoreBrandingService }], propDecorators: { modal: [{ type: ViewChild, args: [ModalComponent, { static: true }] }] } }); class BrandingImportModalService { constructor(modal) { this.modal = modal; } async openBrandingImportModal() { let versionDetails; try { const modalRef = this.modal.show(BrandingImportModalComponent, { class: 'modal-sm', ignoreBackdropClick: true, keyboard: false }); versionDetails = await modalRef.content.result; } catch (e) { // modal closed return; } return versionDetails; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: BrandingImportModalService, deps: [{ token: i1$3.BsModalService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: BrandingImportModalService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: BrandingImportModalService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1$3.BsModalService }] }); class BrandingNameCellRendererComponent { constructor(context, router) { this.router = router; this.name = context.value; this.routerLink = `${this.router.url}/${context.item.name}`; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: BrandingNameCellRendererComponent, deps: [{ token: i3.CellRendererContext }, { token: i2$1.Router }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: BrandingNameCellRendererComponent, isStandalone: true, selector: "c8y-branding-name-cell-renderer", ngImport: i0, template: "<a [routerLink]=\"routerLink\">{{ name }}</a>\n", dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: BrandingNameCellRendererComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-branding-name-cell-renderer', standalone: true, imports: [RouterLink], template: "<a [routerLink]=\"routerLink\">{{ name }}</a>\n" }] }], ctorParameters: () => [{ type: i3.CellRendererContext }, { type: i2$1.Router }] }); class BrandingTagsHeaderCellRendererComponent { constructor() { this.heading = gettext('Applied to'); this.popoverContent = gettext(`<p class="m-b-8">Branding can be applied at two levels</p> <ul class="list-unstyled m-b-0"> <li class="m-b-4"> <strong>Global: </strong> <span>Applied to the entire platform, affecting all apps and interfaces.</span> </li> <li> <strong>App-specific: </strong> <span>Applied only to selected apps within the platform.</span> </li> </ul>`); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: BrandingTagsHeaderCellRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: BrandingTagsHeaderCellRendererComponent, isStandalone: true, selector: "c8y-branding-tags-header-cell-renderer", ngImport: i0, template: "<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", dependencies: [{ kind: "ngmodule", type: PopoverModule }, { kind: "directive", type: i1$4.PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: BrandingTagsHeaderCellRendererComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-branding-tags-header-cell-renderer', standalone: true, imports: [PopoverModule, C8yTranslatePipe], template: "<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" }] }] }); class BrandingComponent { constructor(brandings, activatedRoute, appState, applicationService, zip, staticAssets, router, modal, confirmModal, brandingVersionService, addBrandingModalService, alert, brandingImportModalService, brandingTracking) { this.brandings = brandings; this.activatedRoute = activatedRoute; this.appState = appState; this.applicationService = applicationService; this.zip = zip; this.staticAssets = staticAssets; this.router = router; this.modal = modal; this.confirmModal = confirmModal; this.brandingVersionService = brandingVersionService; this.addBrandingModalService = addBrandingModalService; this.alert = alert; this.brandingImportModalService = brandingImportModalService; this.brandingTracking = brandingTracking; this.pagination = { pageSize: 10, currentPage: 1 }; this.displayOptions = { bordered: false, striped: true, filter: true, gridHeader: true, hover: true }; this.actionControls = [ { type: BuiltInActionType.Edit, callback: (entry, _reload) => { this.editBranding(entry.name); } }, { type: BuiltInActionType.Export, callback: async (entry, _reload) => { await this.exportBranding(entry); } }, { type: 'preview', icon: 'external-link', text: gettext('Open preview'), callback: async (entry, _reload) => { this.brandings.openPreviewForBranding(entry.name); } }, { type: 'duplicate', icon: 'copy', text: gettext('Duplicate'), callback: async (entry, _reload) => { await this.duplicateVersion(entry); this.refresh(); } }, { type: 'apply-to-app', icon: 'form', text: gettext('Apply to specific apps'), callback: async (entry, _reload) => { await this.applyToApps(entry); this.refresh(); } }, { type: 'set-as-default', icon: 'globe1', text: gettext('Set as global'), showIf: (entry) => { if (entry.tags?.includes('latest')) { return false; } return true; }, callback: async (entry, _reload) => { try { await this.confirmModal.confirm(gettext('Set as global branding'), gettext('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?'), Status.INFO); } catch { // did not confirm return; } await this.brandings.markAsActive(entry); this.refresh(); } }, { type: BuiltInActionType.Delete, showIf: (entry) => { if (entry.tags?.includes('latest')) { return false; } return true; }, callback: async (entry, _reload) => { try { await this.confirmModal.confirm(gettext('Delete branding variant'), gettext('You are about to delete this branding variant. This action cannot be undone. Do you want to proceed?'), Status.DANGER); } catch { // did not confirm return; } this.brandingTracking.deleteBrandingVariant(); await this.applicationService.deleteVersionPackage(entry.publicOptionsApp, { version: entry.version }); this.refresh(); } } ]; this.columns = [ { name: 'name', header: gettext('Name'), path: 'name', filterable: true, cellRendererComponent: BrandingNameCellRendererComponent }, { name: gettext('Applied to`Applications a branding applies to (table column header)`'), path: 'tags', filterable: false, cellCSSClassName: 'small', cellRendererComponent: BrandingTagsCellRendererComponent, headerCellRendererComponent: BrandingTagsHeaderCellRendererComponent }, { name: 'owner', header: gettext('Owner'), path: 'owner', filterable: false }, { name: 'lastUpdated', header: gettext('Last updated'), path: 'lastUpdated', filterable: false } ]; this.currentAppContextPath = 'administration'; this.reloadTrigger = new BehaviorSubject(undefined); this.currentAppContextPath = this.appState.state.app?.contextPath || this.currentAppContextPath; this.availableBrandingVariants$ = this.reloadTrigger.pipe(switchMap(() => this.brandings.loadBrandingVariants()), // hide the fallback from users map(variants => { variants.variants = variants.variants.filter(v => !v.tags?.includes('fallback')); return variants; }), shareReplay({ refCount: true, bufferSize: 1 })); } async deleteAllBrandings(publicOptions) { const result = await this.confirmModal.confirm(gettext('Delete all branding variants'), gettext(`<p class="m-b-8"> This action will permanently remove all custom branding variants. Your tenant will revert to the default branding. </p> <p class="m-b-8"> Consider exporting your custom branding variants before proceeding, as this action cannot be undone. </p> <p> Are you sure you want to continue? </p>`), Status.DANGER, { cancel: gettext('Cancel'), ok: gettext('Delete all') }); if (result === false || (typeof result === 'object' && !result.confirmed)) { return; } this.brandingTracking.deleteAllBrandings(); await this.brandings.deleteAllBrandings(publicOptions); this.refresh(); } async exportBranding(variant) { this.brandingTracking.exportBranding(); const branding = await firstValueFrom(this.brandings.getZipForBinary(variant.id)); const staticAssetsApp = await this.staticAssets.getAppForTenant('branding'); const staticAssetsZip = await firstValueFrom(this.brandings.getZipForBinary(staticAssetsApp.activeVersionId, this.staticAssets.fileNames.exportZipName)); const blob = await this.zip.createZip([ { fileName: this.brandings.fileNames.exportZipName, blob: branding }, { fileName: this.staticAssets.fileNames.exportZipName, blob: staticAssetsZip } ]); saveAs(blob, `branding-export-${variant.name}-${variant.revision}-${new Date().getTime()}.zip`); } refresh() { this.reloadTrigger.next(); } async addNewVersion() { const versionDetails = await this.addBrandingModalService.openAddBrandingModal(); if (!versionDetails) { this.refresh(); return; } let fallBackBranding = {}; try { fallBackBranding = await this.brandings.getBrandingOptionsForVersion('latest'); } catch (e) { console.warn(`Failed to load latest branding`); } this.brandingTracking.addNewVersion(); try { await this.brandings.addBranding(this.brandingVersionService.createInitialBrandingVersion(versionDetails.brandingName), fallBackBranding, [versionDetails.brandingName]); await this.brandings.waitForBrandingToBePresent(versionDetails.brandingName); this.editBranding(versionDetails.brandingName); } catch (e) { this.alert.addServerFailure(e, 'danger'); } } async duplicateVersion(version) { const options = await this.brandings.getBrandingOptionsForVersion(version.version); const versionDetails = await this.addBrandingModalService.openDuplicateBrandingModal(); if (!versionDetails) { return; } this.brandingTracking.duplicateVersion(); try { await this.brandings.addBranding(this.brandingVersionService.createInitialBrandingVersion(versionDetails.brandingName), options, [versionDetails.brandingName]); await this.brandings.waitForBrandingToBePresent(versionDetails.brandingName); this.editBranding(versionDetails.brandingName); } catch (e) { this.alert.addServerFailure(e, 'danger'); } } async editBranding(brandingName) { return this.router.navigate([brandingName, 'edit'], { relativeTo: this.activatedRoute }); } async getStartedUsingBranding() { const result = await this.confirmModal.confirm(gettext(`Get started using branding variants`), gettext(`<p class="m-b-8">Confirming this action creates two branding variants:</p> <ul class="m-b-8 p-l-16"> <li>A default copy of your current global branding</li> <li>Your new customizable variant.</li> </ul> <p class="m-b-8"> After customization, you can set your new variant as global or apply it to specific apps. </p> <p> To revert changes, simply set the default branding as global or use the "Delete all variants" button at any time. </p>`)); if (result === false || (typeof result === 'object' && !result.confirmed)) { return; } this.brandingTracking.getStartedUsingBranding(); await this.brandings.getStartedUsingBranding(); await this.addNewVersion(); } async applyToApps(version) { const prefix = 'app-'; const currentTags = version.tags .filter(tag => tag.startsWith(prefix)) .map(tag => tag.replace(prefix, '')); const otherTags = version.tags.filter(tag => !tag.startsWith(prefix)); let selectedTags; try { const modalRef = this.modal.show(ApplyBrandingToAppModalComponent, { initialState: { currentTags }, ignoreBackdropClick: true, keyboard: false }); selectedTags = await modalRef.content.result; } catch (e) { // modal closed return; } const selectedApps = Object.keys(selectedTags).filter(key => selectedTags[key]); this.brandingTracking.applyToApps(selectedApps); const tagsToSet = [...otherTags, ...selectedApps.map(tag => `${prefix}${tag}`)]; await this.removeTagsFromOtherVersions(version.publicOptionsApp, tagsToSet, version.version); await this.applicationService.setPackageVersionTag(version.publicOptionsApp, version.version, tagsToSet); } async importBranding() { try { await this.brandingImportModalService.openBrandingImportModal(); this.brandings.refreshTriggerBrandingVariants.next(); this.reloadTrigger.next(); } catch (e) { // modal closed } } async removeTagsFromOtherVersions(publicOptionsApp, tagsToRemove, versionToIgnore) { for (const version of publicOptionsApp.applicationVersions) { if (version.version === versionToIgnore) { continue; } const removedTags = version.tags.filter(tag => !tagsToRemove.includes(tag)); if (removedTags.length === version.tags.length) { continue; } await this.applicationService.setPackageVersionTag(publicOptionsApp, version.version, removedTags); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: BrandingComponent, deps: [{ token: i1$2.StoreBrandingService }, { token: i2$1.ActivatedRoute }, { token: i3.AppStateService }, { token: i1.ApplicationService }, { token: i3.ZipService }, { token: i3$1.StaticAssetsService }, { token: i2$1.Router }, { token: i1$3.BsModalService }, { token: i3.ModalService }, { token: i1$2.BrandingVersionService }, { token: i1$1.AddBrandingModalService }, { token: i3.AlertService }, { token: BrandingImportModalService }, { token: i1$2.BrandingTrackingService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: BrandingComponent, isStandalone: true, selector: "c8y-branding", ngImport: i0, template: "<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", dependencies: [{ kind: "component", type: BreadcrumbComponent, selector: "c8y-breadcrumb" }, { kind: "component", type: BreadcrumbItemComponent, selector: "c8y-breadcrumb-item", inputs: ["icon", "translate", "label", "path", "injector"] }, { kind: "component", type: TitleComponent, selector: "c8y-title", inputs: ["pageTitleUpdate"] }, { kind: "component", type: HelpComponent, selector: "c8y-help", inputs: ["src", "isCollapsed", "priority", "icon"] }, { kind: "component", type: ActionBarItemComponent, selector: "c8y-action-bar-item", inputs: ["placement", "priority", "itemClass", "injector", "groupId", "inGroupPriority"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "component", type: LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "component", type: DataGridComponent, selector: "c8y-data-grid", inputs: ["title", "loadMoreItemsLabel", "loadingItemsLabel", "showSearch", "refresh", "columns", "rows", "pagination", "childNodePagination", "infiniteScroll", "serverSideDataCallback", "selectable", "singleSelection", "selectionPrimaryKey", "displayOptions", "actionControls", "bulkActionControls", "headerActionControls", "searchText", "configureColumnsEnabled", "showCounterWarning", "activeClassName", "expandableRows", "treeGrid", "hideReload", "childNodesProperty", "parentNodeLabelProperty"], outputs: ["rowMouseOver", "rowMouseLeave", "rowClick", "onConfigChange", "onBeforeFilter", "onBeforeSearch", "onFilter", "itemsSelect", "onReload", "onAddCustomColumn", "onRemoveCustomColumn", "onColumnFilterReset", "onSort", "onPageSizeChange", "onColumnReordered", "onColumnVisibilityChange"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: BrandingComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-branding', standalone: true, imports: [ BreadcrumbComponent, BreadcrumbItemComponent, TitleComponent, HelpComponent, ActionBarItemComponent, IconDirective, C8yTranslateDirective, C8yTranslatePipe, EmptyStateComponent, LoadingComponent, DataGridComponent, AsyncPipe ], template: "<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'\"\