@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
589 lines (583 loc) • 72.2 kB
JavaScript
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> </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> </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> </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