UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

589 lines (583 loc) 72.2 kB
import * as i0 from '@angular/core'; import { output, inject, Component, Injectable, Pipe, input, signal, ViewChild } from '@angular/core'; import { gettext } from '@c8y/ngx-components/gettext'; import * as i2 from '@c8y/ngx-components'; import { ValidationPattern, ChangeIconComponent, C8yTranslatePipe, DocsService, NavigatorService, sortByPriority, DashboardChildComponent, AppSwitcherService, AppHrefPipe, HumanizeAppNamePipe, AppIconComponent, EmptyStateComponent, IconDirective, FormsModule, ListGroupModule, DynamicFormsModule, C8yTranslateModule, ModalService, InterAppService, SupportedApps, Status, DashboardComponent } from '@c8y/ngx-components'; import * as i1$1 from '@angular/common'; import { CommonModule, NgClass } from '@angular/common'; import * as i4 from 'ngx-bootstrap/tooltip'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; import * as i1$2 from 'ngx-bootstrap/popover'; import { PopoverModule } from 'ngx-bootstrap/popover'; import * as i4$1 from 'ngx-bootstrap/collapse'; import { CollapseModule } from 'ngx-bootstrap/collapse'; import { defaultWidgetIds } from '@c8y/ngx-components/widgets/definitions'; import * as i1 from '@angular/forms'; import { FormBuilder, Validators, ReactiveFormsModule, FormsModule as FormsModule$1 } from '@angular/forms'; import { IconSelectorService } from '@c8y/ngx-components/icon-selector'; import { combineLatest, of, take, firstValueFrom, Subject, switchMap as switchMap$1, lastValueFrom, forkJoin } from 'rxjs'; import { isEmpty } from 'lodash'; import { map, switchMap, combineLatestWith, debounceTime, takeUntil } from 'rxjs/operators'; import { ContextDashboardService, WidgetConfigService } from '@c8y/ngx-components/context-dashboard'; import * as i3 from '@angular/cdk/drag-drop'; import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop'; import { TranslateService } from '@ngx-translate/core'; const QuickLinkDisplayOption = { GRID: 'Grid', LIST: 'List' }; const DEFAULT_DISPLAY_OPTION_VALUE = QuickLinkDisplayOption.GRID; const DEFAULT_QUICK_LINK_ICON = 'link'; const HELP_AND_SERVICE_WIDGET_ID = defaultWidgetIds.HELP_AND_SERVICE; const APPLICATIONS_WIDGET_ID = defaultWidgetIds.APPLICATIONS; const QUICK_LINKS_DEVICE_MANAGEMENT_ID = defaultWidgetIds.DEVICE_MANAGEMENT_WELCOME; const URL_VALIDATOR_PATTERN = ValidationPattern.rules.quickLinkUrl.pattern; class QuickLinksWidgetConfigAddLinkComponent { constructor() { this.onQuickLinkCreated = output(); this.onCancel = output(); this.iconSelector = inject(IconSelectorService); this.fb = inject(FormBuilder); } ngOnInit() { this.addLinkFormGroup = this.initForm(); } async changeLinkIcon() { try { const newIcon = await this.iconSelector.selectIcon(); this.addLinkFormGroup.controls.icon.setValue(newIcon); } catch { // nothing to do } } createQuickLink() { this.onQuickLinkCreated.emit(this.addLinkFormGroup.value); this.addLinkFormGroup.reset(); this.addLinkFormGroup.controls.icon.setValue(DEFAULT_QUICK_LINK_ICON); } initForm() { const controls = { icon: [DEFAULT_QUICK_LINK_ICON, [Validators.required]], label: ['', [Validators.required, Validators.maxLength(50)]], url: ['', [Validators.required, Validators.pattern(URL_VALIDATOR_PATTERN)]], newTab: [false, [Validators.required]] }; return this.fb.group(controls); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: QuickLinksWidgetConfigAddLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: QuickLinksWidgetConfigAddLinkComponent, isStandalone: true, selector: "c8y-quick-links-widget-config-add-link", outputs: { onQuickLinkCreated: "onQuickLinkCreated", onCancel: "onCancel" }, ngImport: i0, template: "<form [formGroup]=\"addLinkFormGroup\">\n <div class=\"d-flex a-i-center gap-24\">\n <div class=\"form-group\">\n <label>{{ 'Icon' | translate }}</label>\n <c8y-change-icon\n class=\"form-control\"\n [currentIcon]=\"addLinkFormGroup.controls.icon.value\"\n (onButtonClick)=\"changeLinkIcon()\"\n ></c8y-change-icon>\n </div>\n <div class=\"form-group flex-grow\">\n <label for=\"ql-label\">{{ 'Label' | translate }}</label>\n <input\n class=\"form-control\"\n id=\"ql-label\"\n type=\"text\"\n formControlName=\"label\"\n maxlength=\"50\"\n [placeholder]=\"'e.g. my Quick Link' | translate\"\n />\n </div>\n </div>\n <div class=\"d-flex a-i-center gap-24\">\n <div class=\"form-group flex-grow\">\n <label for=\"ql-url\">{{ 'URL' | translate }}</label>\n <input\n class=\"form-control\"\n id=\"ql-url\"\n type=\"text\"\n formControlName=\"url\"\n maxlength=\"150\"\n [placeholder]=\"'e.g. http://www.example.com' | translate\"\n />\n </div>\n <div class=\"form-group flex-noshrink\">\n <label>&nbsp;</label>\n <label\n class=\"c8y-checkbox\"\n title=\"{{ 'Open the link in a new browser tab' | translate }}\"\n >\n <input\n [attr.aria-label]=\"'Open in new tab' | translate\"\n type=\"checkbox\"\n formControlName=\"newTab\"\n checked=\"checked\"\n />\n <span></span>\n <span>{{ 'New tab' | translate }}</span>\n </label>\n </div>\n </div>\n</form>\n\n<button\n class=\"btn btn-default btn-sm\"\n type=\"button\"\n (click)=\"onCancel.emit()\"\n>\n {{ 'Cancel' | translate }}\n</button>\n<button\n class=\"btn btn-primary btn-sm\"\n title=\"{{ 'Create a quick link' | translate }}\"\n type=\"button\"\n (click)=\"createQuickLink()\"\n [disabled]=\"addLinkFormGroup.invalid\"\n>\n {{ 'Add quick link' | translate }}\n</button>\n", dependencies: [{ kind: "ngmodule", type: TooltipModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: ChangeIconComponent, selector: "c8y-change-icon", inputs: ["currentIcon"], outputs: ["onButtonClick"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: QuickLinksWidgetConfigAddLinkComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-quick-links-widget-config-add-link', imports: [TooltipModule, C8yTranslatePipe, ReactiveFormsModule, ChangeIconComponent], template: "<form [formGroup]=\"addLinkFormGroup\">\n <div class=\"d-flex a-i-center gap-24\">\n <div class=\"form-group\">\n <label>{{ 'Icon' | translate }}</label>\n <c8y-change-icon\n class=\"form-control\"\n [currentIcon]=\"addLinkFormGroup.controls.icon.value\"\n (onButtonClick)=\"changeLinkIcon()\"\n ></c8y-change-icon>\n </div>\n <div class=\"form-group flex-grow\">\n <label for=\"ql-label\">{{ 'Label' | translate }}</label>\n <input\n class=\"form-control\"\n id=\"ql-label\"\n type=\"text\"\n formControlName=\"label\"\n maxlength=\"50\"\n [placeholder]=\"'e.g. my Quick Link' | translate\"\n />\n </div>\n </div>\n <div class=\"d-flex a-i-center gap-24\">\n <div class=\"form-group flex-grow\">\n <label for=\"ql-url\">{{ 'URL' | translate }}</label>\n <input\n class=\"form-control\"\n id=\"ql-url\"\n type=\"text\"\n formControlName=\"url\"\n maxlength=\"150\"\n [placeholder]=\"'e.g. http://www.example.com' | translate\"\n />\n </div>\n <div class=\"form-group flex-noshrink\">\n <label>&nbsp;</label>\n <label\n class=\"c8y-checkbox\"\n title=\"{{ 'Open the link in a new browser tab' | translate }}\"\n >\n <input\n [attr.aria-label]=\"'Open in new tab' | translate\"\n type=\"checkbox\"\n formControlName=\"newTab\"\n checked=\"checked\"\n />\n <span></span>\n <span>{{ 'New tab' | translate }}</span>\n </label>\n </div>\n </div>\n</form>\n\n<button\n class=\"btn btn-default btn-sm\"\n type=\"button\"\n (click)=\"onCancel.emit()\"\n>\n {{ 'Cancel' | translate }}\n</button>\n<button\n class=\"btn btn-primary btn-sm\"\n title=\"{{ 'Create a quick link' | translate }}\"\n type=\"button\"\n (click)=\"createQuickLink()\"\n [disabled]=\"addLinkFormGroup.invalid\"\n>\n {{ 'Add quick link' | translate }}\n</button>\n" }] }], propDecorators: { onQuickLinkCreated: [{ type: i0.Output, args: ["onQuickLinkCreated"] }], onCancel: [{ type: i0.Output, args: ["onCancel"] }] } }); /** * Service for managing quick links in Cockpit and Device Management applications. * It fetches, processes, and provides quick links to relevant documentation and navigation nodes. */ class QuickLinksService { constructor() { this.docsService = inject(DocsService); this.navigatorService = inject(NavigatorService); this.labelsToFilterOutInDeviceManagement = [ 'Add user', 'Add device', 'Cockpit', 'Legal notices' ]; } /** * Retrieves the default quick links for Cockpit Application. * * @returns An observable emitting an array of quick links. */ getDefaultQuickLinks$() { return this.getDocLinks$().pipe(map(docLinks => docLinks.map(docLink => ({ icon: docLink.icon, label: docLink.label, url: docLink.url, newTab: docLink.target === '_blank' })))); } /** * Retrieves default quick links for Device Management Application * * @returns An observable emitting an array of quick links for device management. */ getQuickLinksForDeviceManagement$() { return this.getSortedDocLinksForDeviceManagement().pipe(map(docLinks => docLinks.map(docLink => ({ icon: docLink.icon, label: docLink.label, url: docLink.url, newTab: docLink.target === '_blank' })))); } /** * Fetches documentation links for Cockpit Application and sorts them by priority. * * @returns An observable emitting an array of documentation links. */ getDocLinks$() { return combineLatest([this.docsService.items$, this.navigatorService.items$]).pipe(map(([links, navigatorNodes]) => this.handleDocLinks([...links], navigatorNodes)), map(docLinks => sortByPriority(docLinks))); } /** * Processes and modifies documentation links. * * @param links - Array of documentation links. * @param navigatorNodes - Array of navigation nodes. * * @returns An array of processed documentation links. */ handleDocLinks(links, navigatorNodes) { const groupLink = this.createAddGroupDocLink(navigatorNodes); if (groupLink) { links.push(groupLink); } return this.replaceDocsLinksWithMainOne(links); } /** * Creates a quick link for adding a group if the "Groups" node is present. * * @param navigatorNodes - Array of navigation nodes. * @returns A `DocLink` for adding a group or `undefined`. */ createAddGroupDocLink(navigatorNodes) { let docLink; const groupsNodeLabel = gettext('Groups'); const groupsNode = this.findNavigatorNode(groupsNodeLabel, navigatorNodes); if (groupsNode) { docLink = { type: 'quicklink', icon: 'c8y-icon c8y-icon-group-add', label: gettext('Add group'), target: null, url: '/group?showAddGroup=true' }; } return docLink; } /** * Retrieves additional documentation links related to device management. * * @returns An observable that emits a list of processed documentation links. */ getAdditionalDocLinksForDeviceManagement$() { return this.docsService.items$.pipe(map(docLinks => this.prepareDocLinksForDeviceManagement(docLinks))); } /** * Prepares documentation links by replacing some links with a main one and filtering out irrelevant links. * * @param links - The list of documentation links to process. * @returns The processed list of documentation links. */ prepareDocLinksForDeviceManagement(links) { const _links = this.replaceDocsLinksWithMainOne(links); return this.filterOutDocsLinksForDeviceManagement(_links); } /** * Filters out documentation links that should not be included in device management. * * @param links - The list of documentation links to filter. * @returns The filtered list of documentation links. */ filterOutDocsLinksForDeviceManagement(links) { const filteredLinks = links.filter(link => !this.labelsToFilterOutInDeviceManagement.includes(link.label) && !link.url.includes('/apps/')); const additionalLinks = this.docsService .getItemsFromHookDocs() .filter(doc => this.labelsToFilterOutInDeviceManagement.includes(doc.label)); return sortByPriority([...filteredLinks, ...additionalLinks]); } /** * Replaces the first occurrence of a documentation link with a main user guide link. * * @param links - The list of documentation links to process. * @returns The modified list of documentation links. */ replaceDocsLinksWithMainOne(links) { const DOCS_PATH = '/docs/'; const docsLinkRegex = /\/docs\/(?!legal-notices)/; let firstDocsLink = true; return links.reduce((acc, link) => { const isDocsLink = link.url && docsLinkRegex.test(link.url); if (isDocsLink) { if (firstDocsLink) { firstDocsLink = false; // Replace the first /docs/ link with the main one acc.push({ icon: 'book-shelf', label: gettext('User documentation'), url: this.docsService.getUserGuideLink(DOCS_PATH), type: 'doc', target: '_blank' }); } } else { acc.push({ ...link, target: this.isLinkForCurrentApp(link) ? null : '_blank' }); } return acc; }, []); } /** * Checks if a URL is valid. * * @param url - The URL string to validate. * @returns `true` if the URL is valid, otherwise `false`. */ isValidURL(url) { try { new URL(url); return true; } catch { return false; } } /** * Determines if a link belongs to the current application. * * @param link - The documentation link to check. * @returns `true` if the link is for the current app, otherwise `false`. */ isLinkForCurrentApp(link) { const { url } = link; if (!url) return false; if (!this.isValidURL(url)) { return !url.startsWith('/apps/'); } return false; } /** * Finds a navigation node by its label. * * @param nodeName - Label of the node to find. * @param navNodes - Array of navigation nodes. * * @returns The found navigation node. */ findNavigatorNode(nodeName, navNodes = []) { return navNodes.find((node) => node.label === nodeName); } /** * Retrieves the default quick links for device management. * * This method returns an array of predefined quick link definitions * used for navigation within the device management section of the application. * * Each quick link is defined by a navigation path (`navPath`), and optionally * includes override properties such as a custom label, icon, or URL. * * @returns An array of quick link definitions. */ getDefaultQuickLinksForDeviceManagement() { return [ { navPath: ['Devices', 'All devices'] }, { navPath: ['Devices', 'Registration'], overrides: { label: gettext('Register device') } }, { navPath: ['Groups'], overrides: { label: gettext('Add group'), icon: 'c8y-group-add', url: '/group?showAddGroup=true' } }, { navPath: ['Management', 'Device profiles'], overrides: { label: gettext('Add device profile') } }, { navPath: ['Management', 'Software repository'], overrides: { label: gettext('Add software') } }, { navPath: ['Management', 'Firmware repository'], overrides: { label: gettext('Add firmware') } } ]; } /** * Fetches documentation links for Device Management Application and sorts them by priority. * * @returns An observable emitting an array of documentation links. */ getSortedDocLinksForDeviceManagement() { return this.navigatorService.items$.pipe(switchMap(navNodes => { return of(this.getDefaultQuickLinksForDeviceManagement() .map(({ navPath, overrides }) => this.createDocLinkToNavNode(navPath, overrides, navNodes)) .filter(Boolean)); }), combineLatestWith(this.getAdditionalDocLinksForDeviceManagement$()), map(([links, additionalLinks]) => [...links, ...additionalLinks]), map(docLinks => sortByPriority(docLinks))); } /** * Creates a document link based on the given navigation node path labels. * * @param navNodePathLabels - An array of strings representing the path labels used to find the navigation node. * @param quickLinkOverrides - An optional partial object of `DocLink` properties that can override the defaults. * @param navNodes - An array of `NavigatorNode` objects to search within for the navigation node. * * @returns A `DocLink` object with the details of the found navigation node, or `undefined` if no matching node is found. */ createDocLinkToNavNode(navNodePathLabels, quickLinkOverrides = {}, navNodes) { const navNode = this.findVisibleNavNode(navNodePathLabels, navNodes); if (!navNode) { return; } return { icon: navNode.icon, type: 'doc', label: navNode.label, priority: navNode.priority, url: this.ensureLeadingSlash(navNode.path), ...quickLinkOverrides }; } /** * Ensures the given path starts with a leading slash. * * @param path - The path to check. * @returns The path with a leading slash. */ ensureLeadingSlash(path) { return path.startsWith('/') ? path : `/${path}`; } /** * Recursively searches for a visible navigation node that matches the given path labels. * * @param navNodePathLabels - An array of labels representing the navigation path. * This array is mutated as elements are shifted during recursion. * @param navNodes - An array of `NavigatorNode` objects to search within. Defaults to `this.navNodes`. * * @returns The found `NavigatorNode`. */ findVisibleNavNode(navNodePathLabels, navNodes = this.navNodes) { const currentLabel = navNodePathLabels.shift(); const navNode = navNodes.find(navNode => !navNode.hidden && navNode.label === currentLabel); if (navNode && navNodePathLabels.length > 0) { return this.findVisibleNavNode(navNodePathLabels, navNode.children); } return navNode; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: QuickLinksService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: QuickLinksService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: QuickLinksService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class RelativeUrlParserPipe { transform(url) { if (!url) { return ''; } if (this.isAppUrl(url) || this.isFullUrl(url)) { return url; } if (this.isQueryParameter(url)) { return `${window.location.hash}${url}`; } if (this.isRelativeToBaseUrl(url)) { return url; } if (this.isRelativeUrl(url)) { return `#${url}`; } return url; } isFullUrl(url) { return url.startsWith('http') || url.startsWith('https'); } isRelativeToBaseUrl(url) { return url.startsWith('#'); } isRelativeUrl(url) { return url.startsWith('/'); } isAppUrl(url) { return url.startsWith('/apps'); } isQueryParameter(url) { return url.startsWith('?'); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RelativeUrlParserPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: RelativeUrlParserPipe, isStandalone: true, name: "relativeUrlParser" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RelativeUrlParserPipe, decorators: [{ type: Pipe, args: [{ name: 'relativeUrlParser' }] }] }); class QuickLinksWidgetViewComponent { constructor() { this.config = input(...(ngDevMode ? [undefined, { debugName: "config" }] : [])); this.isPreview = input(false, ...(ngDevMode ? [{ debugName: "isPreview" }] : [])); this.DEFAULT_QUICK_LINK_ICON = DEFAULT_QUICK_LINK_ICON; this.DisplayOption = QuickLinkDisplayOption; this.quickLinksService = inject(QuickLinksService); this.dashboardChild = inject(DashboardChildComponent); this.appSwitcherService = inject(AppSwitcherService); this.appHrefPipe = inject(AppHrefPipe); this.humanizeAppNamePipe = inject(HumanizeAppNamePipe); } ngOnInit() { this.convertLegacyWidget(); } /** * The method is responsible for converting legacy widgets into their updated versions if a conversion is required. * * The widgets are being converted: * - Help and Service Widget * - Applications Widget * - Quick Links - Device Management Widget */ convertLegacyWidget() { if (!this.isConversionRequiredForWidget()) { return; } const widgetId = this.getWidgetId(); const widgetConversionMap = { [HELP_AND_SERVICE_WIDGET_ID]: { convertWidget: this.convertHelpAndServiceWidget.bind(this) }, [APPLICATIONS_WIDGET_ID]: { convertWidget: this.convertApplicationsWidget.bind(this) }, [QUICK_LINKS_DEVICE_MANAGEMENT_ID]: { convertWidget: this.convertDeviceManagementQuickLinksWidget.bind(this) } }; const widget = widgetConversionMap[widgetId]; widget.convertWidget(); } /** * Converts the Device Management Quick Links widget by assigning default quick links for the device management app * and updating the widget configuration with default options. */ convertDeviceManagementQuickLinksWidget() { this.quickLinksService .getQuickLinksForDeviceManagement$() .pipe(take(1)) .subscribe(quickLinks => (this.config().links = quickLinks)); this.config().displayOption = DEFAULT_DISPLAY_OPTION_VALUE; } /** * Converts the Applications widget by assigning default quick links * and updating the widget configuration with default options. */ convertApplicationsWidget() { this.appSwitcherService.apps$.pipe(take(1)).subscribe(async (oneCloudApps) => (this.config().links = await Promise.all(oneCloudApps.map(async (app) => ({ label: await firstValueFrom(this.humanizeAppNamePipe.transform(app)), url: this.appHrefPipe.transform(app), icon: null, newTab: false, app }))))); this.config().displayOption = DEFAULT_DISPLAY_OPTION_VALUE; } /** * Converts the Help and Service widget by assigning default quick links * and updating the widget configuration with default options. */ convertHelpAndServiceWidget() { this.quickLinksService .getDefaultQuickLinks$() .pipe(take(1)) .subscribe(quickLinks => (this.config().links = quickLinks)); this.config().displayOption = DEFAULT_DISPLAY_OPTION_VALUE; } /** * Determines whether conversion is required for the widget. * Conversion is needed if the widget configuration is empty and a valid widget ID exists. * * @returns `true` if conversion is required, otherwise `false`. */ isConversionRequiredForWidget() { return isEmpty(this.config()) && !!this.getWidgetId(); } /** * Retrieves the widget ID from the dashboard child data. * If `componentId` is available, it is returned; otherwise, the widget's `name` is used. * * @returns The widget ID as a `ConvertibleWidgetID`, based on `componentId` or `name`. */ getWidgetId() { return this.dashboardChild['data']?.componentId ? this.dashboardChild['data']?.componentId : this.dashboardChild['data']?.name; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: QuickLinksWidgetViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: QuickLinksWidgetViewComponent, isStandalone: true, selector: "c8y-quick-links-widget-view", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, isPreview: { classPropertyName: "isPreview", publicName: "isPreview", isSignal: true, isRequired: false, transformFunction: null } }, providers: [AppHrefPipe, HumanizeAppNamePipe, RelativeUrlParserPipe], ngImport: i0, template: "@let links = config().links;\n@let pointerNoneStylesPreview = isPreview() ? { 'pointer-events': 'none' } : null;\n\n@if (config().displayOption === DisplayOption.GRID) {\n @if (links?.length) {\n <div class=\"card-group-block interact-grid border-top m-b-0\">\n @for (link of links; track link) {\n @let linkLabel = config().translateLinkLabels ? (link.label | translate) : link.label;\n <a\n class=\"card card--btn pointer\"\n [ngStyle]=\"pointerNoneStylesPreview\"\n [title]=\"linkLabel\"\n [ngClass]=\"{\n disabled: isPreview()\n }\"\n data-cy=\"c8y-quick-links-widget-view--quick-link-card\"\n [attr.role]=\"isPreview() ? null : 'button'\"\n [target]=\"!isPreview() && link.newTab ? '_blank' : '_self'\"\n [attr.rel]=\"isPreview() ? null : 'noopener noreferrer'\"\n [attr.href]=\"isPreview() ? null : (link.url | relativeUrlParser)\"\n >\n @if (link.newTab && !isPreview()) {\n <div\n class=\"card-actions showOnHover\"\n title=\"{{ 'Open in new tab' | translate }}\"\n >\n <span class=\"dropdown-toggle c8y-dropdown\">\n <i c8yIcon=\"external-link\"></i>\n </span>\n </div>\n }\n\n <div class=\"card-block text-center\">\n <div class=\"icon-32\">\n @if (link.icon) {\n <i\n class=\"c8y-icon-duocolor\"\n [c8yIcon]=\"link.icon\"\n ></i>\n } @else {\n <c8y-app-icon\n [name]=\"link.app.name\"\n [app]=\"link.app\"\n [contextPath]=\"link.app.contextPath\"\n ></c8y-app-icon>\n }\n </div>\n <small class=\"text-muted\">\n {{ linkLabel }}\n </small>\n </div>\n </a>\n }\n </div>\n } @else {\n <c8y-ui-empty-state\n [icon]=\"DEFAULT_QUICK_LINK_ICON\"\n [title]=\"'No quick links to display.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n }\n} @else {\n @if (links?.length) {\n <div class=\"separator-top\">\n @for (link of links; track link) {\n @let linkLabel = config().translateLinkLabels ? (link.label | translate) : link.label;\n <a\n class=\"d-flex a-i-center btn-clean gap-8 p-16 text-truncate separator-bottom\"\n [ngStyle]=\"pointerNoneStylesPreview\"\n [title]=\"linkLabel\"\n data-cy=\"c8y-quick-links-widget-view--quick-link-list-item\"\n [attr.role]=\"isPreview() ? null : 'button'\"\n [target]=\"!isPreview() && link.newTab ? '_blank' : '_self'\"\n [attr.rel]=\"isPreview() ? null : 'noopener noreferrer'\"\n [attr.href]=\"isPreview() ? null : (link.url | relativeUrlParser)\"\n >\n @if (link.icon) {\n <i\n class=\"c8y-icon-duocolor icon-24\"\n [c8yIcon]=\"link.icon\"\n ></i>\n } @else {\n <c8y-app-icon\n [name]=\"link.app.name\"\n [app]=\"link.app\"\n [contextPath]=\"link.app.contextPath\"\n ></c8y-app-icon>\n }\n\n <span\n class=\"text-truncate\"\n [title]=\"linkLabel\"\n >\n {{ linkLabel }}\n </span>\n @if (link.newTab) {\n <i\n class=\"text-muted m-l-auto showOnHover\"\n [c8yIcon]=\"'external-link'\"\n title=\"{{ 'Open in new tab' | translate }}\"\n ></i>\n }\n </a>\n }\n </div>\n } @else {\n <c8y-ui-empty-state\n [icon]=\"DEFAULT_QUICK_LINK_ICON\"\n [title]=\"'No quick links to display.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n }\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: AppIconComponent, selector: "c8y-app-icon", inputs: ["contextPath", "name", "app"] }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: RelativeUrlParserPipe, name: "relativeUrlParser" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: QuickLinksWidgetViewComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-quick-links-widget-view', imports: [ CommonModule, AppIconComponent, RelativeUrlParserPipe, EmptyStateComponent, IconDirective, C8yTranslatePipe ], providers: [AppHrefPipe, HumanizeAppNamePipe, RelativeUrlParserPipe], template: "@let links = config().links;\n@let pointerNoneStylesPreview = isPreview() ? { 'pointer-events': 'none' } : null;\n\n@if (config().displayOption === DisplayOption.GRID) {\n @if (links?.length) {\n <div class=\"card-group-block interact-grid border-top m-b-0\">\n @for (link of links; track link) {\n @let linkLabel = config().translateLinkLabels ? (link.label | translate) : link.label;\n <a\n class=\"card card--btn pointer\"\n [ngStyle]=\"pointerNoneStylesPreview\"\n [title]=\"linkLabel\"\n [ngClass]=\"{\n disabled: isPreview()\n }\"\n data-cy=\"c8y-quick-links-widget-view--quick-link-card\"\n [attr.role]=\"isPreview() ? null : 'button'\"\n [target]=\"!isPreview() && link.newTab ? '_blank' : '_self'\"\n [attr.rel]=\"isPreview() ? null : 'noopener noreferrer'\"\n [attr.href]=\"isPreview() ? null : (link.url | relativeUrlParser)\"\n >\n @if (link.newTab && !isPreview()) {\n <div\n class=\"card-actions showOnHover\"\n title=\"{{ 'Open in new tab' | translate }}\"\n >\n <span class=\"dropdown-toggle c8y-dropdown\">\n <i c8yIcon=\"external-link\"></i>\n </span>\n </div>\n }\n\n <div class=\"card-block text-center\">\n <div class=\"icon-32\">\n @if (link.icon) {\n <i\n class=\"c8y-icon-duocolor\"\n [c8yIcon]=\"link.icon\"\n ></i>\n } @else {\n <c8y-app-icon\n [name]=\"link.app.name\"\n [app]=\"link.app\"\n [contextPath]=\"link.app.contextPath\"\n ></c8y-app-icon>\n }\n </div>\n <small class=\"text-muted\">\n {{ linkLabel }}\n </small>\n </div>\n </a>\n }\n </div>\n } @else {\n <c8y-ui-empty-state\n [icon]=\"DEFAULT_QUICK_LINK_ICON\"\n [title]=\"'No quick links to display.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n }\n} @else {\n @if (links?.length) {\n <div class=\"separator-top\">\n @for (link of links; track link) {\n @let linkLabel = config().translateLinkLabels ? (link.label | translate) : link.label;\n <a\n class=\"d-flex a-i-center btn-clean gap-8 p-16 text-truncate separator-bottom\"\n [ngStyle]=\"pointerNoneStylesPreview\"\n [title]=\"linkLabel\"\n data-cy=\"c8y-quick-links-widget-view--quick-link-list-item\"\n [attr.role]=\"isPreview() ? null : 'button'\"\n [target]=\"!isPreview() && link.newTab ? '_blank' : '_self'\"\n [attr.rel]=\"isPreview() ? null : 'noopener noreferrer'\"\n [attr.href]=\"isPreview() ? null : (link.url | relativeUrlParser)\"\n >\n @if (link.icon) {\n <i\n class=\"c8y-icon-duocolor icon-24\"\n [c8yIcon]=\"link.icon\"\n ></i>\n } @else {\n <c8y-app-icon\n [name]=\"link.app.name\"\n [app]=\"link.app\"\n [contextPath]=\"link.app.contextPath\"\n ></c8y-app-icon>\n }\n\n <span\n class=\"text-truncate\"\n [title]=\"linkLabel\"\n >\n {{ linkLabel }}\n </span>\n @if (link.newTab) {\n <i\n class=\"text-muted m-l-auto showOnHover\"\n [c8yIcon]=\"'external-link'\"\n title=\"{{ 'Open in new tab' | translate }}\"\n ></i>\n }\n </a>\n }\n </div>\n } @else {\n <c8y-ui-empty-state\n [icon]=\"DEFAULT_QUICK_LINK_ICON\"\n [title]=\"'No quick links to display.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n }\n}\n" }] }], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], isPreview: [{ type: i0.Input, args: [{ isSignal: true, alias: "isPreview", required: false }] }] } }); class QuickLinksWidgetConfigListComponent { constructor() { this.quickLinksForm = input(...(ngDevMode ? [undefined, { debugName: "quickLinksForm" }] : [])); this.config = input(...(ngDevMode ? [undefined, { debugName: "config" }] : [])); this.quickLinksFormArray = input(...(ngDevMode ? [undefined, { debugName: "quickLinksFormArray" }] : [])); this.appsNameChanged = input(...(ngDevMode ? [undefined, { debugName: "appsNameChanged" }] : [])); this.iconSelector = inject(IconSelectorService); } drop(event) { moveItemInArray(this.config().links, event.previousIndex, event.currentIndex); moveItemInArray(this.quickLinksFormArray().controls, event.previousIndex, event.currentIndex); } async changeLinkIcon(linkForm) { try { const isIconPresent = !!linkForm.get('icon')?.value; const newIcon = await this.iconSelector.selectIcon(); if (newIcon) { linkForm.patchValue({ icon: newIcon }); this.config().links = this.getQuickLinks(); } const isAppIconChange = !!linkForm.get('app')?.value; if (isAppIconChange && !isIconPresent) { this.appsNameChanged().update(apps => [...apps, linkForm.get('app')?.value]); } } catch { // Nothing to do if the user cancels icon selection } } removeLink(index) { const quickLinksFormArray = this.quickLinksForm().get('quickLinks'); quickLinksFormArray.removeAt(index); this.config().links = this.getQuickLinks(); } getQuickLinks() { return this.quickLinksForm().getRawValue().quickLinks; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: QuickLinksWidgetConfigListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: QuickLinksWidgetConfigListComponent, isStandalone: true, selector: "c8y-quick-links-widget-config-list", inputs: { quickLinksForm: { classPropertyName: "quickLinksForm", publicName: "quickLinksForm", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, quickLinksFormArray: { classPropertyName: "quickLinksFormArray", publicName: "quickLinksFormArray", isSignal: true, isRequired: false, transformFunction: null }, appsNameChanged: { classPropertyName: "appsNameChanged", publicName: "appsNameChanged", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "d-contents" }, ngImport: i0, template: "<c8y-list-group\n class=\"cdk-droplist no-border-last separator-top\"\n cdkDropList\n (cdkDropListDropped)=\"drop($event)\"\n [cdkDropListDisabled]=\"getQuickLinks().length < 2\"\n>\n <form\n class=\"d-contents\"\n [formGroup]=\"quickLinksForm()\"\n >\n <div\n class=\"d-contents\"\n [formArrayName]=\"'quickLinks'\"\n >\n @for (link of quickLinksFormArray().controls; track link; let i = $index) {\n @let linkValue = link.getRawValue();\n\n <c8y-li\n [dense]=\"true\"\n [formGroupName]=\"i\"\n cdkDrag\n >\n <c8y-li-drag-handle\n title=\"{{ 'Drag to reorder' | translate }}\"\n cdkDragHandle\n >\n <i c8yIcon=\"drag-reorder\"></i>\n </c8y-li-drag-handle>\n <c8y-li-icon\n class=\"icon-24 p-relative changeIcon a-s-stretch\"\n [ngClass]=\"{\n 'm-l-16': getQuickLinks().length < 2\n }\"\n >\n @if (linkValue.icon) {\n <c8y-change-icon\n [currentIcon]=\"linkValue.icon\"\n (onButtonClick)=\"changeLinkIcon(link)\"\n ></c8y-change-icon>\n } @else {\n <c8y-change-icon (onButtonClick)=\"changeLinkIcon(link)\">\n <c8y-app-icon\n [name]=\"linkValue.app.name\"\n [app]=\"linkValue.app\"\n [contextPath]=\"linkValue.app.contextPath\"\n ></c8y-app-icon>\n </c8y-change-icon>\n }\n </c8y-li-icon>\n\n <div class=\"d-flex gap-8 a-i-center\">\n <div class=\"input-group input-group-editable\">\n <input\n class=\"form-control\"\n formControlName=\"label\"\n [placeholder]=\"'e.g. my Quick Link' | translate\"\n />\n <span></span>\n </div>\n\n <button\n class=\"showOnHover btn-dot btn-dot--danger m-l-auto\"\n [attr.aria-label]=\"'Delete' | translate\"\n tooltip=\"{{ 'Delete' | translate }}\"\n placement=\"top\"\n type=\"button\"\n [delay]=\"500\"\n (click)=\"removeLink(i)\"\n >\n <i c8yIcon=\"minus-circle\"></i>\n </button>\n </div>\n <c8y-list-item-collapse>\n <div class=\"d-flex a-i-center gap-24\">\n <div class=\"form-group flex-grow\">\n <label for=\"ql-url\">{{ 'URL' | translate }}</label>\n <input\n class=\"form-control\"\n id=\"ql-url\"\n type=\"text\"\n formControlName=\"url\"\n maxlength=\"150\"\n [placeholder]=\"'e.g. https://www.example.com' | translate\"\n />\n </div>\n <div class=\"form-group flex-noshrink\">\n <label>&nbsp;</label>\n <label\n class=\"c8y-checkbox\"\n title=\"{{ 'Open the link in a new browser tab' | translate }}\"\n >\n <input\n [attr.aria-label]=\"'Open in new tab' | translate\"\n type=\"checkbox\"\n formControlName=\"newTab\"\n checked=\"checked\"\n />\n <span></span>\n <span>{{ 'New tab' | translate }}</span>\n </label>\n </div>\n </div>\n </c8y-list-item-collapse>\n </c8y-li>\n }\n </div>\n </form>\n</c8y-list-group>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i2.RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i3.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i3.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i3.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i4.TooltipDirective, selector: "[tooltip], [tooltipHtml]", inputs: ["adaptivePosition", "tooltip", "placement", "triggers", "container", "containerClass", "boundariesElement", "isOpen", "isDisabled", "delay", "tooltipHtml", "tooltipPlacement", "tooltipIsOpen", "tooltipEnable", "tooltipAppendToBody", "tooltipAnimation", "tooltipClass", "tooltipContext", "tooltipPopupDelay", "tooltipFadeDuration", "tooltipTrigger"], outputs: ["tooltipChange", "onShown", "onHidden", "tooltipStateChanged"], exportAs: ["bs-tooltip"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "ngmodule", type: ListGroupModule }, { kind: "component", type: i2.ListGroupComponent, selector: "c8y-list-group" }, { kind: "component", type: i2.ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: i2.ListItemIconComponent, selector: "c8y-list-item-icon, c8y-li-icon", inputs: ["icon", "status"] }, { kind: "component", type: i2.ListItemCollapseComponent, selector: "c8y-list-item-collapse, c8y-li-collapse", inputs: ["collapseWay"] }, { kind: "component", type: i2.ListItemDragHandleComponent, selector: "c8y-list-item-drag-handle, c8y-li-drag-handle" }, { kind: "ngmodule", type: DynamicFormsModule }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i1.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "component", type: AppIconComponent, selector: "c8y-app-icon", inputs: ["contextPath", "name", "app"] }, { kind: "ngmodule", type: C8yTranslateModule }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "component", type: ChangeIconComponent, selector: "c8y-change-icon", inputs: ["currentIcon"], outputs: ["onButtonClick"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: i2.C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: QuickLinksWidgetConfigListComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-quick-links-widget-config-list', host: { class: 'd-contents' }, imports: [ FormsModule, DragDropModule, TooltipModule, PopoverModule, ListGroupModule, DynamicFormsModule, AppIconComponent, C8yTranslateModule, IconDirective, ChangeIconComponent, NgClass ], template: "<c8y-list-group\n class=\"cdk-dro