@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
2,927 lines • 163 kB
JavaScript
import { gettext } from '@c8y/ngx-components/gettext';
import * as i0 from '@angular/core';
import { InjectionToken, inject, Injectable, Directive, Injector, runInInjectionContext, Pipe, viewChildren, ViewContainerRef, effect, reflectComponentType, Input, Component, viewChild, EventEmitter, ContentChild, Output } from '@angular/core';
import { InventoryService } from '@c8y/client';
import * as i2 from '@c8y/ngx-components';
import { hookGeneric, ExtensionPointForPlugins, GroupService, fromTriggerOnce, getInjectedHooks, stateToFactory, AssetTypesRealtimeService, AlertService, isPromise, IconDirective, C8yTranslatePipe, ListGroupModule, EmptyStateComponent, BottomDrawerRef, FormsModule as FormsModule$1, LoadMoreComponent, TabComponent, TabsOutletComponent, BottomDrawerService } from '@c8y/ngx-components';
import { isArray, isObjectLike, isEmpty, find, forOwn, get, isUndefined, cloneDeep } from 'lodash-es';
import { firstValueFrom, mergeMap, filter, take, map, distinctUntilChanged, shareReplay, BehaviorSubject, of, from, switchMap, isObservable, Subject, takeUntil, debounceTime } from 'rxjs';
import * as i1 from '@angular/router';
import { NgFor, NgClass, NgTemplateOutlet, AsyncPipe, NgIf } from '@angular/common';
import { CdkDropList, CdkDrag } from '@angular/cdk/drag-drop';
import * as i1$1 from '@angular/forms';
import { NgForm, FormsModule } from '@angular/forms';
import * as i3 from 'ngx-bootstrap/tooltip';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import * as i4 from '@angular/cdk/tree';
import { CdkTreeModule } from '@angular/cdk/tree';
import { DataSource } from '@angular/cdk/collections';
import { TranslateService } from '@ngx-translate/core';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
const defaultAssetPropertyListConfig = {
filterable: true,
selectMode: 'none',
expansionMode: 'expandedByDefault',
showHeader: true,
showValue: true,
showKey: true,
emptyStateContent: 'empty',
allowAddingCustomProperties: false,
inputPropertiesHandle: 'merge',
allowDragAndDrop: false
};
const defaultAssetProperties = [
{
c8y_JsonSchema: { properties: { name: { type: 'string', label: 'Name' } } },
name: 'name',
label: gettext('Name'),
type: 'string',
active: true,
isEditable: true,
isStandardProperty: true
},
{
c8y_JsonSchema: { properties: { id: { type: 'string', label: 'ID' } } },
name: 'id',
label: gettext('ID'),
type: 'string',
active: true,
isEditable: false,
isStandardProperty: true
},
{
c8y_JsonSchema: {
properties: { type: { type: 'string', label: 'Type' } }
},
name: 'type',
label: gettext('Type'),
type: 'string',
active: true,
isEditable: false,
isStandardProperty: true
},
{
c8y_JsonSchema: {
properties: { owner: { type: 'string', label: 'Owner' } }
},
name: 'owner',
label: gettext('Owner'),
type: 'string',
isEditable: false,
isStandardProperty: true
},
{
c8y_JsonSchema: {
properties: { lastUpdated: { type: 'string', label: 'Last updated' } }
},
name: 'lastUpdated',
label: gettext('Last updated'),
type: 'string',
isEditable: false,
isStandardProperty: true
}
];
const deviceAssetProperties = [
{
label: gettext('Active alarms status'),
type: 'object',
isEditable: false,
isStandardProperty: true,
name: 'c8y_ActiveAlarmsStatus',
c8y_JsonSchema: {
properties: {
c8y_ActiveAlarmsStatus: {
key: 'c8y_ActiveAlarmsStatus',
type: 'object',
label: 'Active alarms status',
properties: {
critical: {
title: 'Critical',
type: 'number'
},
major: {
title: 'Major',
type: 'number'
},
minor: {
title: 'Minor',
type: 'number'
},
warning: {
title: 'Warning',
type: 'number'
}
}
}
}
}
},
{
label: gettext('Address'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_Address',
c8y_JsonSchema: {
properties: {
c8y_Address: {
key: 'c8y_Address',
type: 'object',
label: 'Address',
properties: {
street: {
title: 'Street',
type: 'string'
},
city: {
title: 'City',
type: 'string'
},
cityCode: {
title: 'City code',
type: 'string'
},
territory: {
title: 'Territory',
type: 'string'
},
region: {
title: 'Region',
type: 'string'
},
country: {
title: 'Country',
type: 'string'
}
}
}
}
}
},
{
label: gettext('Agent'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_Agent',
c8y_JsonSchema: {
properties: {
c8y_Agent: {
key: 'c8y_Agent',
type: 'object',
label: 'Agent',
properties: {
name: {
title: 'Name',
type: 'string'
},
version: {
title: 'Version',
type: 'string'
},
url: {
title: 'URL',
type: 'string'
},
maintainer: {
title: 'Maintainer',
type: 'string'
}
}
}
}
}
},
{
label: gettext('Availability'),
type: 'object',
isEditable: false,
isStandardProperty: true,
name: 'c8y_Availability',
c8y_JsonSchema: {
properties: {
c8y_Availability: {
key: 'c8y_Availability',
type: 'object',
label: 'Availability',
properties: {
status: {
title: 'Status',
type: 'string'
},
lastMessage: {
title: 'Last message',
type: 'string',
printFormat: 'datetime'
}
}
}
}
}
},
{
label: gettext('Connection'),
type: 'object',
isEditable: false,
isStandardProperty: true,
name: 'c8y_Connection',
c8y_JsonSchema: {
properties: {
c8y_Connection: {
key: 'c8y_Connection',
type: 'object',
label: 'Connection',
properties: {
status: {
title: 'Status',
type: 'string'
}
}
}
}
}
},
{
label: gettext('Communication mode'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_CommunicationMode',
c8y_JsonSchema: {
properties: {
c8y_CommunicationMode: {
key: 'c8y_CommunicationMode',
type: 'object',
label: 'Communication mode',
properties: {
mode: {
title: 'Mode',
type: 'string'
}
}
}
}
}
},
{
label: gettext('Firmware'),
type: 'object',
isEditable: false,
isStandardProperty: true,
name: 'c8y_Firmware',
c8y_JsonSchema: {
properties: {
c8y_Firmware: {
key: 'c8y_Firmware',
type: 'object',
label: 'Firmware',
properties: {
moduleVersion: {
title: 'Module version',
type: 'string'
},
name: {
title: 'Name',
type: 'string'
},
version: {
title: 'Version',
type: 'string'
},
url: {
title: 'URL',
type: ['string', 'null']
}
}
}
}
}
},
{
label: gettext('Hardware'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_Hardware',
c8y_JsonSchema: {
properties: {
c8y_Hardware: {
key: 'c8y_Hardware',
type: 'object',
label: 'Hardware',
properties: {
model: {
title: 'Model',
type: 'string'
},
serialNumber: {
title: 'Serial number',
type: 'string'
},
revision: {
title: 'Revision',
type: 'string'
}
}
}
}
}
},
{
label: gettext('LPWAN device'),
type: 'object',
isEditable: false,
isStandardProperty: true,
name: 'c8y_LpwanDevice',
c8y_JsonSchema: {
properties: {
c8y_LpwanDevice: {
key: 'c8y_LpwanDevice',
type: 'object',
label: 'LPWAN device',
properties: {
provisioned: {
title: 'Provisioned',
type: 'boolean'
}
}
}
}
}
},
{
label: gettext('Mobile'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_Mobile',
c8y_JsonSchema: {
properties: {
c8y_Mobile: {
key: 'c8y_Mobile',
type: 'object',
label: 'Mobile',
properties: {
cellId: {
title: 'Cell ID',
type: ['string', 'null']
},
connType: {
title: 'Connection type',
type: 'string',
readOnly: true
},
currentOperator: {
title: 'Current operator',
type: 'string',
readOnly: true
},
currentBand: {
title: 'Current band',
type: 'string',
readOnly: true
},
ecn0: {
title: 'ECN0',
type: 'string',
readOnly: true
},
iccid: {
title: 'ICCID',
type: ['string', 'null']
},
imei: {
title: 'IMEI',
type: ['string', 'null']
},
imsi: {
title: 'IMSI',
type: ['string', 'null']
},
lac: {
title: 'LAC',
type: ['string', 'null']
},
mcc: {
title: 'MCC',
type: ['string', 'null']
},
mnc: {
title: 'MNC',
type: ['string', 'null']
},
msisdn: {
title: 'MSISDN',
type: 'string'
},
rcsp: {
title: 'RCSP',
type: 'string',
readOnly: true
},
rscp: {
title: 'RSCP',
type: 'string',
readOnly: true
},
rsrp: {
title: 'RSRP',
type: 'string',
readOnly: true
},
rsrq: {
title: 'RSRQ',
type: 'string',
readOnly: true
},
rssi: {
title: 'RSSI',
type: 'string',
readOnly: true
}
}
}
}
}
},
{
name: 'c8y_Notes',
label: 'Notes',
type: 'string',
isEditable: true,
isStandardProperty: true,
c8y_JsonSchema: {
properties: {
c8y_Notes: {
type: 'string',
label: 'Notes',
'x-schema-form': {
type: 'textarea'
}
}
}
}
},
{
label: gettext('Position'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_Position',
c8y_JsonSchema: {
properties: {
c8y_Position: {
key: 'c8y_Position',
type: 'object',
label: 'Position',
properties: {
lat: {
title: 'Latitude',
type: 'number'
},
lng: {
title: 'Longitude',
type: 'number'
},
alt: {
title: 'Altitude',
type: 'number'
}
}
}
}
}
},
{
label: gettext('Required availability'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_RequiredAvailability',
c8y_JsonSchema: {
properties: {
c8y_RequiredAvailability: {
key: 'c8y_RequiredAvailability',
type: 'object',
label: 'Required availability',
properties: {
responseInterval: {
title: 'Response interval',
description: 'Takes a value between -32768 and 32767 minutes (a negative value indicates that the device is under maintenance).',
type: 'integer',
minimum: -32768,
maximum: 32767
}
}
}
}
}
},
{
label: gettext('Software'),
type: 'object',
isEditable: false,
isStandardProperty: true,
name: 'c8y_Software',
c8y_JsonSchema: {
properties: {
c8y_Software: {
key: 'c8y_Software',
type: 'object',
label: 'Software',
properties: {
name: {
title: 'Name',
type: 'string'
},
version: {
title: 'Version',
type: 'string'
},
url: {
title: 'URL',
type: ['string', 'null']
}
}
}
}
}
},
{
label: gettext('Network'),
type: 'object',
isEditable: true,
isStandardProperty: true,
name: 'c8y_Network',
c8y_JsonSchema: {
properties: {
c8y_Network: {
key: 'c8y_Network',
type: 'object',
label: 'Network',
properties: {
c8y_DHCP: {
title: 'DHCP',
type: 'object',
printFormat: 'hidden',
name: 'c8y_DHCP',
properties: {
addressRange: {
title: 'Address range',
type: 'object',
name: 'addressRange',
printFormat: 'hidden',
properties: {
start: {
title: 'Start',
type: 'string'
},
end: {
title: 'End',
type: 'string'
}
}
},
dns1: {
title: 'DNS 1',
type: 'string'
},
dns2: {
title: 'DNS 2',
type: 'string'
},
enabled: {
title: 'Enabled',
type: 'integer'
}
}
},
c8y_LAN: {
title: 'LAN',
type: 'object',
name: 'c8y_LAN',
printFormat: 'hidden',
properties: {
enabled: {
title: 'Enabled',
type: 'integer'
},
ip: {
title: 'IP',
type: 'string'
},
mac: {
title: 'MAC',
type: 'string'
},
name: {
title: 'Name',
type: 'string'
},
netmask: {
title: 'Netmask',
type: 'string'
}
}
},
c8y_WAN: {
title: 'WAN',
type: 'object',
name: 'c8y_WAN',
printFormat: 'hidden',
properties: {
apn: {
title: 'APN',
type: 'string'
},
authType: {
title: 'Auth type',
type: 'string'
},
ip: {
title: 'IP',
type: 'string'
},
password: {
title: 'Password',
type: 'string'
},
simStatus: {
title: 'SIM status',
type: 'string'
},
username: {
title: 'Username',
type: 'string'
}
}
}
}
}
}
}
}
];
const RESULT_TYPES = {
VALUE: { name: 'VALUE', value: 1, label: gettext('Only value') },
VALUE_UNIT: { name: 'VALUE_UNIT', value: 2, label: gettext('Value and unit') },
VALUE_UNIT_TIME: { name: 'VALUE_UNIT_TIME', value: 3, label: gettext('Value, unit and time') }
};
const HOOK_COMPUTED_PROPERTY = new InjectionToken('HOOK_COMPUTED_PROPERTY');
function hookComputedProperty(property, options) {
return hookGeneric(property, HOOK_COMPUTED_PROPERTY, options);
}
class ComputedPropertiesService extends ExtensionPointForPlugins {
constructor(rootInjector, router, plugins) {
super(rootInjector, plugins);
this.router = router;
this.groupService = inject(GroupService);
this.items$ = this.setupItemsObservable();
}
/**
* Returns the current state.
* @readonly
* @returns The current set of computed properties.
*/
get state() {
return this.state$.value;
}
add(propertyDef) {
this.state.add(propertyDef);
this.emitNewState();
}
getByName(name) {
if (!name) {
return Promise.resolve(undefined);
}
return firstValueFrom(this.items$.pipe(mergeMap((propertyDefs) => propertyDefs), filter((propertyDef) => propertyDef.name === name), take(1)));
}
getByContext(asset) {
if (!asset) {
return Promise.resolve([]);
}
const computedPropertyContextType = this.getTypeOfContext(asset);
return firstValueFrom(this.items$.pipe(map((propertyDefs) => propertyDefs.filter((propertyDef) => {
if (!propertyDef.contextType) {
return true;
}
return propertyDef.contextType.includes(computedPropertyContextType);
}))));
}
getTypeOfContext(context) {
const isEvent = (item) => !('severity' in item);
const isAlarm = (item) => 'severity' in item;
if (this.groupService.isDevice(context)) {
return 'device';
}
else if (this.groupService.isGroup(context) &&
!this.groupService.isAsset(context)) {
return 'group';
}
else if (this.groupService.isAsset(context)) {
return 'asset';
}
else if (isAlarm(context)) {
return 'alarm';
}
else if (isEvent(context)) {
return 'event';
}
}
setupItemsObservable() {
return fromTriggerOnce(this.router, this.refresh$, [
getInjectedHooks(HOOK_COMPUTED_PROPERTY, this.injectors),
() => this.factories,
stateToFactory(this.state$)
]).pipe(distinctUntilChanged(), shareReplay(1));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ComputedPropertiesService, deps: [{ token: i0.Injector }, { token: i1.Router }, { token: i2.PluginsResolveService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ComputedPropertiesService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ComputedPropertiesService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: i0.Injector }, { type: i1.Router }, { type: i2.PluginsResolveService }] });
/**
* Service for managing asset properties.
*/
class AssetPropertiesService {
constructor() {
this.FRAGMENTS_TO_OMIT = [
'additionParents',
'assetParents',
'deviceParents',
'childAdditions',
'childAssets',
'childDevices',
'c8y_IsDevice',
'__children',
'c8y_ui',
'self',
'parent',
'c8y_DataPoint',
'c8y_Kpi_Migrated',
/^c8y_Dashboard!\d+/
];
this.inventoryService = inject(InventoryService);
this.assetTypesRealtimeService = inject(AssetTypesRealtimeService);
this.groupService = inject(GroupService);
this.alert = inject(AlertService);
this.computedPropertiesService = inject(ComputedPropertiesService);
}
/**
* Filters added properties to only include those compatible with the given asset.
* Currently only checks compatibility for computed properties.
* @param allAddedProperties All properties that have been added by the user
* @param asset The current asset context
* @returns Promise resolving to properties compatible with the asset
*/
async filterCompatibleProperties(allAddedProperties, asset) {
if (!asset || !allAddedProperties.length) {
return allAddedProperties;
}
let availableComputedPropertyNames = null;
const computedProperties = allAddedProperties.filter(prop => prop.computed);
if (computedProperties.length > 0) {
const availableComputedProperties = await this.computedPropertiesService.getByContext(asset);
availableComputedPropertyNames = new Set(availableComputedProperties.map(({ prop }) => prop.name));
}
const compatibleProperties = [];
for (const property of allAddedProperties) {
if (await this.isPropertyAvailable(property, availableComputedPropertyNames)) {
compatibleProperties.push(property);
}
}
return compatibleProperties;
}
/**
* Retrieves properties for an asset from asset library.
* @param asset The asset for which to retrieve custom properties.
* @returns A promise resolving to the list of custom properties.
*/
async requestAssetProperties(asset) {
if (asset && asset.type) {
const assetType = await firstValueFrom(this.assetTypesRealtimeService.getAssetTypeByName$(asset.type));
if (assetType) {
const { data } = await this.inventoryService.childAdditionsList(assetType, {
pageSize: 2000,
query: "$filter=(has('c8y_IsAssetProperty'))"
});
return data;
}
}
return [];
}
/**
* Retrieves the initial set of properties for an asset, based on its type.
* @param asset The asset for which to retrieve properties.
* @returns A promise resolving to the list of initial properties.
*/
async getInitialProperties(asset) {
if (!asset) {
return [];
}
else if (this.groupService.isDevice(asset)) {
return await this.getDeviceProperties(asset);
}
else if (this.groupService.isGroup(asset) && !this.groupService.isAsset(asset)) {
return await this.getGroupProperties(asset);
}
else if (this.groupService.isAsset(asset)) {
return await this.getAssetProperties(asset);
}
return [];
}
/**
* Retrieves properties for a device asset.
* @param asset The device asset for which to retrieve properties.
* @returns A promise resolving to the list of device properties.
*/
async getDeviceProperties(asset) {
return this.getManagedObjectProperties(asset);
}
/**
* Retrieves properties for a group asset.
* @param asset The group asset for which to retrieve properties.
* @returns A promise resolving to the list of group properties.
*/
async getGroupProperties(asset) {
return this.getManagedObjectProperties(asset);
}
/**
* Retrieves properties for a regular asset.
* @param asset The asset for which to retrieve properties.
* @returns A promise resolving to the list of asset properties.
*/
async getAssetProperties(asset) {
const customProperties = this.categorizeCustomProperties(await this.requestAssetProperties(asset)) || [];
return customProperties;
}
/**
* Categorizes custom properties into simple and complex types.
* @param properties The custom properties to categorize.
* @returns The categorized custom properties.
*/
categorizeCustomProperties(properties) {
const { simple, complex } = properties.reduce((acc, property) => {
const schema = property.c8y_JsonSchema.properties[property.name];
if (schema.type === 'object') {
acc.complex.push(property);
}
else {
acc.simple.push(property);
}
return acc;
}, { simple: [], complex: [] });
return [...simple, ...complex];
}
/**
* Categorizes and flattens hierarchical properties into simple and complex types.
* @param properties The hierarchical properties to categorize and flatten.
* @returns The categorized and flattened properties.
*/
categorizeAndFlattenHierarchicalProperties(properties) {
const sortedProperties = [...properties].sort((a, b) => {
const aLabel = (a.label || a.name || '').toLowerCase();
const bLabel = (b.label || b.name || '').toLowerCase();
return aLabel.localeCompare(bLabel);
});
const result = sortedProperties.reduce((acc, property) => {
property.active = false;
if (property.computed) {
acc.computed.push(property);
}
else if (this.isComplexProperty(property)) {
acc.complex.push(property);
this.addNestedProperties(property, acc.complex, true);
}
else {
acc.simple.push(property);
}
return acc;
}, { computed: [], simple: [], complex: [] });
return result;
}
/**
* Checks if a property is complex (i.e., has nested properties).
* @param property The property to check.
* @returns True if the property is complex, false otherwise.
*/
isComplexProperty(property) {
return (property.c8y_JsonSchema?.properties[property.name]?.type === 'object' ||
property.properties !== undefined);
}
/**
* Checks if property is available based on provided available computed property names.
* @param property The property to check.
* @param availableComputedPropertyNames Set of available computed property names.
* @returns True if the property is available, false otherwise.
*/
async isPropertyAvailable(property, availableComputedPropertyNames) {
if (property.computed) {
if (!availableComputedPropertyNames) {
return false;
}
return availableComputedPropertyNames.has(property.name);
}
return true;
}
/**
* Checks if two properties match for selection purposes.
* @param property1 First property to compare.
* @param property2 Second property to compare.
* @returns True if properties match.
*/
propertiesMatch(property1, property2) {
if (property1.name === property2.name) {
const keyPath1 = property1.keyPath;
const keyPath2 = property2.keyPath;
if (keyPath1 && keyPath2) {
return keyPath1.join('.') === keyPath2.join('.');
}
else if (!keyPath1 && !keyPath2) {
if (property1.computed &&
property2.computed &&
property1.instanceId &&
property2.instanceId) {
return property1.instanceId === property2.instanceId;
}
return true;
}
}
return false;
}
/**
* Retrieves custom properties from the properties library, optionally filtered by search text.
* @param searchText Optional search text to filter properties.
* @returns A promise resolving to the list of properties and paging information.
*/
async getPropertiesFromPropertiesLibrary(searchText) {
const propertiesFromLibrary = [];
const { data: propertiesMOs, paging } = await this.requestPropertiesFromPropertiesLibrary(searchText);
propertiesMOs.forEach(prop => {
const name = Object.keys(prop.c8y_JsonSchema.properties)[0];
propertiesFromLibrary.push({
c8y_JsonSchema: prop.c8y_JsonSchema,
label: prop.name,
name,
type: prop.c8y_JsonSchema.properties[name].type
});
});
return { propertiesFromLibrary, paging };
}
async requestPropertiesFromPropertiesLibrary(searchText) {
let queryFilter = '(type eq c8y_JsonSchema) and (appliesTo.MANAGED_OBJECTS eq true)';
if (searchText?.trim()) {
const escapedSearchText = searchText.replace(/'/g, "''");
queryFilter += ` and (name eq '*${escapedSearchText}*')`;
}
const query = `$filter=(${queryFilter})`;
const filter = {
pageSize: 20,
revert: true,
query,
withTotalPages: true,
withTotalElements: true
};
try {
const results = await this.inventoryService.list(filter);
return results;
}
catch (error) {
this.alert.addServerFailure(error);
return null;
}
}
addNestedProperties(parentProperty, complexProperties, sortChildren = false) {
const schema = parentProperty.c8y_JsonSchema?.properties[parentProperty.name];
if (!schema?.properties) {
return;
}
this.flattenProperties(schema, complexProperties, parentProperty.name, [], sortChildren);
}
flattenProperties(schema, result, parentName, parentPath = [], sortChildren = false) {
const properties = schema.properties?.[parentName]?.properties || schema.properties;
let entries = Object.entries(properties);
if (sortChildren) {
entries = entries.sort((a, b) => {
const aLabel = (a[1].title || a[1].label || a[1].name || a[0] || '').toLowerCase();
const bLabel = (b[1].title || b[1].label || b[1].name || b[0] || '').toLowerCase();
return aLabel.localeCompare(bLabel);
});
}
entries.forEach(([key, property]) => {
const path = parentPath.includes(parentName) ? parentPath : [...parentPath, parentName];
result.push({ ...property, keyPath: [...path, key] });
if (property.properties) {
this.flattenProperties({ properties: { [key]: property } }, result, key, [...path, key], sortChildren);
}
});
}
getManagedObjectProperties(asset) {
return this.extractFragments(asset);
}
extractFragments(object) {
const properties = [];
for (const [key, value] of Object.entries(object)) {
if (this.shouldSkipFragment(key) ||
isArray(value) ||
(isObjectLike(value) && isEmpty(value))) {
continue;
}
const newProp = {
label: key,
name: key,
type: isObjectLike(value) ? 'object' : 'string',
isEditable: true,
c8y_JsonSchema: {
properties: {
[key]: {
key: key,
type: isObjectLike(value) ? 'object' : 'string',
label: key,
properties: {}
}
}
}
};
if (isObjectLike(value)) {
this.addPropertyItem(newProp.c8y_JsonSchema.properties[key].properties, value);
}
properties.push(newProp);
}
return properties;
}
shouldSkipFragment(key) {
return !!find(this.FRAGMENTS_TO_OMIT, (fragmentToOmit) => {
if (fragmentToOmit instanceof RegExp) {
return fragmentToOmit.test(key);
}
return fragmentToOmit === key;
});
}
addPropertyItem(properties, object) {
if (!properties) {
return;
}
forOwn(object, (value, key) => {
properties[key] = { title: key, type: value ? typeof value : 'string' };
if (isObjectLike(value)) {
properties[key].type = 'object';
this.addPropertyItem(properties[key]['properties'], value);
}
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertiesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertiesService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertiesService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
class FlatTreeDataSource extends DataSource {
constructor() {
super();
this._dataChange = new BehaviorSubject([]);
}
get data() {
return this._dataChange.value;
}
set data(value) {
this._dataChange.next(value);
}
connect() {
return this._dataChange.asObservable();
}
disconnect() {
// No need to unsubscribe from the _dataChange subject since it's a BehaviorSubject
}
}
class AssetPropertyActionDirective {
constructor(template, elementRef, viewContainer) {
this.template = template;
this.elementRef = elementRef;
this.viewContainer = viewContainer;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyActionDirective, deps: [{ token: i0.TemplateRef }, { token: i0.ElementRef }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.15", type: AssetPropertyActionDirective, isStandalone: true, selector: "[c8yAssetPropertyAction]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyActionDirective, decorators: [{
type: Directive,
args: [{
selector: '[c8yAssetPropertyAction]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.TemplateRef }, { type: i0.ElementRef }, { type: i0.ViewContainerRef }] });
/**
* Formats the value of an asset property.
* If the property is complex, it will be stringified.
* If the property has a keyPath, it will be used to retrieve the value from the asset.
* Otherwise, the value will be taken directly from the asset.
* If the value is null or undefined, a dash ('-') will be returned.
*/
class AssetPropertyValuePipe {
constructor() {
this.assetPropertiesService = inject(AssetPropertiesService);
this.computedPropertiesService = inject(ComputedPropertiesService);
this.injector = inject(Injector);
}
transform(property, context) {
if (property.computed) {
return this.getCallbackComputedPropertyValue(property, context).pipe(map(val => this.formatPropertyValue(val)));
}
if (!property) {
return of('-');
}
let value;
if (this.assetPropertiesService.isComplexProperty(property)) {
value = JSON.stringify(context[property.name]);
}
else if ('keyPath' in property) {
value = this.formatPropertyValue(get(context, property.keyPath));
}
else {
value = this.formatPropertyValue(context[property.name]);
}
return of(value ?? '-');
}
formatPropertyValue(value) {
if (value == null) {
return null;
}
return typeof value === 'string' ? value : JSON.stringify(value);
}
getCallbackComputedPropertyValue(property, context) {
return from(this.computedPropertiesService.getByName(property.name)).pipe(switchMap(definition => {
let value = '-';
runInInjectionContext(definition.injector || this.injector, () => {
value = definition.value({ config: property.config, context });
});
if (isObservable(value) || value instanceof Promise) {
return value;
}
else {
return of(value);
}
}));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyValuePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyValuePipe, isStandalone: true, name: "c8yAssetPropertyValue" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyValuePipe, decorators: [{
type: Pipe,
args: [{
name: 'c8yAssetPropertyValue',
standalone: true
}]
}] });
/**
* Pipe to transform asset property types into icon names.
* Maps various property types to corresponding icon names.
*/
class AssetPropertyIconPipe {
transform(assetProperty) {
if (assetProperty.computed) {
return 'bolt';
}
const type = assetProperty.type;
switch (type) {
case 'string':
return 'string';
case 'number':
return 'hashtag';
case 'boolean':
return 'true-false';
case 'date':
return 'calendar';
case 'object':
case 'c8y_JsonSchema':
return 'c8y-css';
case 'enum':
return 'content';
case 'file':
return 'file';
case 'integer':
return 'unit';
default:
return 'mix-words';
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyIconPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyIconPipe, isStandalone: true, name: "c8yAssetPropertyIcon" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyIconPipe, decorators: [{
type: Pipe,
args: [{
name: 'c8yAssetPropertyIcon',
standalone: true
}]
}] });
/**
* A pipe that transforms an asset property type into a tooltip string.
* If the property is temporary, it appends "(temporary)" to the type and handles translation.
* Otherwise, it returns the type as is.
*/
class AssetPropertyIconTooltipPipe {
constructor() {
this.translateService = inject(TranslateService);
}
transform(property) {
if (!property) {
return '';
}
else if (property.temporary) {
return this.translateService.instant(gettext('{{assetPropertyType}}\n(temporary)'), {
assetPropertyType: property.type
});
}
else {
return property.type;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyIconTooltipPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyIconTooltipPipe, isStandalone: true, name: "c8yAssetPropertyIconTooltip" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyIconTooltipPipe, decorators: [{
type: Pipe,
args: [{
name: 'c8yAssetPropertyIconTooltip',
standalone: true
}]
}] });
class ComputedPropertiesConfigComponent {
constructor() {
this.properties = [];
this.definitions = [];
this.hosts = viewChildren('host', ...(ngDevMode ? [{ debugName: "hosts", read: ViewContainerRef }] : [{ read: ViewContainerRef }]));
this.configForms = viewChildren('configForm', ...(ngDevMode ? [{ debugName: "configForms", read: NgForm }] : [{ read: NgForm }]));
this.propertyConfigs = [];
this.result = new Promise((resolve, reject) => {
this._close = resolve;
this._cancel = reject;
});
this.injector = inject(Injector);
this.bsModalRef = inject(BsModalRef);
effect(async () => {
const hosts = this.hosts();
if (!hosts || hosts.length === 0) {
return;
}
for (let i = 0; i < this.propertyConfigs.length; i++) {
const propertyConfig = this.propertyConfigs[i];
const host = hosts[i];
if (host && propertyConfig.definition) {
await this.createConfigComponent(propertyConfig, host);
}
}
});
}
ngOnInit() {
this.propertyConfigs = this.properties.map((property, index) => ({
property,
definition: this.definitions[index],
config: property.config || this.definitions[index].prop.config
}));
}
get isFormValid() {
return this.configForms().every(form => form.valid) ?? false;
}
async apply() {
const allValid = await this.callOnBeforeSaveHooks();
if (allValid) {
const configs = this.propertyConfigs.map(pc => pc.config);
this._close(configs);
this.bsModalRef.hide();
}
}
cancel() {
this.bsModalRef.hide();
this._cancel();
}
async createConfigComponent(propertyConfig, host) {
const component = await this.getComponent(propertyConfig.definition);
host.clear();
const componentRef = host.createComponent(component, {
environmentInjector: this.injector
});
propertyConfig.componentRef = componentRef;
propertyConfig.componentInstance = componentRef.instance;
const componentMetadata = this.safeReflectComponentType(component);
const hasConfigInput = componentMetadata?.inputs.some(input => input.propName === 'config');
if (hasConfigInput) {
componentRef.setInput('config', propertyConfig.config);
}
else {
propertyConfig.componentInstance.config = propertyConfig.config;
}
const hasAssetInput = componentMetadata?.inputs.some(input => input.propName === 'asset');
if (hasAssetInput) {
componentRef.setInput('asset', this.asset);
}
else {
propertyConfig.componentInstance.asset = this.asset;
}
}
safeReflectComponentType(componentType) {
try {
return reflectComponentType(componentType);
}
catch (error) {
console.warn('Failed to reflect component metadata:', error);
return { inputs: [] };
}
}
async getComponent(definition) {
if (!!definition.loadConfigComponent) {
return await definition.loadConfigComponent();
}
else if (!!definition.configComponent) {
return definition.configComponent;
}
}
async callOnBeforeSaveHooks() {
const validationObservables = this.propertyConfigs
.filter(pc => pc.componentRef)
.map(pc => this.callSingleOnBeforeSaveHook(pc.componentRef, pc.config));
try {
const validationPromises = validationObservables.map(obs => firstValueFrom(obs));
const results = await Promise.all(validationPromises);
return results.every(result => result === true);
}
catch (error) {
return false;
}
}
callSingleOnBeforeSaveHook(componentRef, config) {
if (!componentRef) {
return of(true);
}
const hook = componentRef.instance.onBeforeSave;
if (hook) {
const result = hook.call(componentRef.instance, config);
if (isUndefined(result)) {
return of(true);
}
if (isPromise(result)) {
return from(result);
}
return (isObservable(result) ? result : of(result));
}
return of(true);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ComputedPropertiesConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.15", type: ComputedPropertiesConfigComponent, isStandalone: true, selector: "c8y-computed-properties-config", inputs: { properties: "properties", definitions: "definitions", asset: "asset" }, viewQueries: [{ propertyName: "hosts", predicate: ["host"], descendants: true, read: ViewContainerRef, isSignal: true }, { propertyName: "configForms", predicate: ["configForm"], descendants: true, read: NgForm, isSignal: true }], ngImport: i0, template: "<div class=\"viewport-modal has-asset-selector\">\n <div class=\"modal-header dialog-header\">\n <i [c8yIcon]=\"'bolt'\"></i>\n <div\n class=\"modal-title\"\n id=\"modal-title\"\n translate\n >\n Configure computed properties\n </div>\n </div>\n <div\n class=\"inner-scroll\"\n id=\"modal-body\"\n >\n <p\n class=\"p-t-16 p-l-24 p-r-24 p-b-16 bg-component separator-bottom sticky-top text-center text-balance\"\n translate\n >\n Configure the settings below for each computed property. These configurations determine how\n the properties will be calculated and displayed. All required fields must be completed before\n you can apply the changes.\n </p>\n <div class=\"p-24 p-t-0\">\n <fieldset\n class=\"property-config-section c8y-fieldset p-b-8\"\n *ngFor=\"let propertyConfig of propertyConfigs; let i = index\"\n >\n <legend>\n {{ (propertyConfig.property.label || propertyConfig.property.name) | translate }}\n <span class=\"a-s-center m-l-8 tag tag--default\">{{ propertyConfig.property.name }}</span>\n </legend>\n <form\n class=\"property-config-form\"\n name=\"configForm{{ i }}\"\n #configForm=\"ngForm\"\n >\n <ng-template #host></ng-template>\n </form>\n </fieldset>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button\n class=\"btn btn-default\"\n [title]=\"'Cancel' | translate\"\n type=\"button\"\n (click)=\"cancel()\"\n >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n [title]=\"'Apply' | translate\"\n type=\"button\"\n (click)=\"apply()\"\n [disabled]=\"!isFormValid\"\n >\n {{ 'Apply' | translate }}\n </button>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ComputedPropertiesConfigComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-computed-properties-config', standalone: true, imports: [C8yTranslatePipe, IconDirective, FormsModule, NgFor], template: "<div class=\"viewport-modal has-asset-selector\">\n <div class=\"modal-header dialog-header\">\n <i [c8yIcon]=\"'bolt'\"></i>\n <div\n class=\"modal-title\"\n id=\"modal-title\"\n translate\n >\n Configure computed properties\n </div>\n </div>\n <div\n class=\"inner-scroll\"\n id=\"modal-body\"\n >\n <p\n class=\"p-t-16 p-l-24 p-r-24 p-b-16 bg-component separator-bottom sticky-top text-center text-balance\"\n translate\n >\n Configure the settings below for each computed property. These configurations determine how\n the properties will be calculated and displayed. All required fields must be completed before\n you can apply the changes.\n </p>\n <div class=\"p-24 p-t-0\">\n <fieldset\n class=\"property-config-section c8y-fieldset p-b-8\"\n *ngFor=\"let propertyConfig of propertyConfigs; let i = index\"\n >\n <legend>\n {{ (propertyConfig.property.label || propertyConfig.property.name) | translate }}\n <span class=\"a-s-center m-l-8 tag tag--default\">{{ propertyConfig.property.name }}</span>\n </legend>\n <form\n class=\"property-config-form\"\n name=\"configForm{{ i }}\"\n #configForm=\"ngForm\"\n >\n <ng-template #host></ng-template>\n </form>\n </fieldset>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button\n class=\"btn btn-default\"\n [title]=\"'Cancel' | translate\"\n type=\"button\"\n (click)=\"cancel()\"\n >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n [title]=\"'Apply' | translate\"\n type=\"button\"\n (click)=\"apply()\"\n [disabled]=\"!isFormValid\"\n >\n {{ 'Apply' | translate }}\n </button>\n </div>\n</div>\n" }]
}], ctorParameters: () => [], propDecorators: { properties: [{
type: Input
}], definitions: [{
type: Input
}], asset: [{
type: Input
}], hosts: [{ type: i0.ViewChildren, args: ['host', { ...{ read: ViewContainerRef }, isSignal: true }] }], configForms: [{ type: i0.ViewChildren, args: ['configForm', { ...{ read: NgForm }, isSignal: true }] }] } });
/**
* Pipe to get the title for the collapse/expand button of a tree node.
*/
class AssetPropertyCollapseButtonTitlePipe {
constructor() {
this.collapseLabel = gettext('Collapse "{{ assetPropertyLabel }}"');
this.expandLabel = gettext('Expand "{{ assetPropertyLabel }}"');
this.translateService = inject(TranslateService);
}
transform(expanded, node) {
if (!node) {
return '';
}
else {
const template = expanded ? this.collapseLabel : this.expandLabel;
const assetPropertyLabel = node.property.label || node.property.title || node.property.name;
return this.translateService.instant(template, {
assetPropertyLabel: this.translateService.instant(assetPropertyLabel)
});
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyCollapseButtonTitlePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyCollapseButtonTitlePipe, isStandalone: true, name: "c8yAssetPropertyCollapseButtonTitle" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyCollapseButtonTitlePipe, decorators: [{
type: Pipe,
args: [{
name: 'c8yAssetPropertyCollapseButtonTitle',
standalone: true
}]
}] });
class AssetPropertyDragDropService {
/**
* Main method to handle the drag and drop operation
*/
handleDrop(context) {
if (context.previousIndex === context.currentIndex) {
return null;
}
if (!this.validateDrop(context)) {
return null;
}
return this.performMove(context);
}
/**
* Validates if the drop operation is allowed
*/
validateDrop(context) {
const { draggedNode } = context;
// Get source and target information
const draggedLevel = this.getNodeLevel(draggedNode);
const dropPosition = this.calculateDropPosition(context);
// Validation 1: Check level compatibility
if (!this.validateLevelCompatibility(draggedLevel, dropPosition.targetLevel)) {
return false;
}
// Validation 2: For children, check parent compatibility
if (draggedLevel > 0) {
if (!this.validateParentCompatibility(draggedNode, dropPosition.targetParentPath, draggedLevel)) {
return false;
}
}
return true;
}
/**
* Validates that source and target levels match
*/
validateLevelCompatibility(sourceLevel, targetLevel) {
return sourceLevel === targetLevel;
}
/**
* Validates that child nodes stay within their parent
*/
validateParentCompatibility(node, targetParentPath, nodeLevel) {
const sourceParentPath = this.getParentPath(node, nodeLevel);
return !!targetParentPath && sourceParentPath === targetParentPath;
}
/**
* Performs the actual move operation
*/
performMove(context) {
const { draggedNode } = context;
return this.isExpandableNode(draggedNode)
? this.moveNodeWithChildren(context)
: this.moveSingleNode(context);
}
/**
* Moves a single node without children
*/
moveSingleNode(context) {
const result = [...context.data];
const [removed] = result.splice(context.previousIndex, 1);
result.splice(context.currentIndex, 0, removed);
return result;
}
/**
* Moves a parent node along with all its children
*/
moveNodeWithChildren(context) {
const { draggedNode, previousIndex, currentIndex, data } = context;
// Find all children of the parent
const childIndices = this.findChildIndices(draggedNode, data, previousIndex);
// Collect all nodes to move (parent + children)
const nodesToMove = this.collectNodesToMove(data, previousIndex, childIndices);
// Remove nodes from their current position
const remainingNodes = this.removeNodesFromData(data, previousIndex, childIndices);
// Calculate adjusted insertion index
const insertIndex = this.calculateAdjustedInsertIndex(previousIndex, currentIndex, childIndices);
// Insert nodes at new position
remainingNodes.splice(insertIndex, 0, ...nodesToMove);
return remainingNodes;
}
/**
* Finds indices of all children of a parent node
*/
findChildIndices(parentNode, data, parentIndex) {
const childIndices = [];
const parentKey = this.getNodeKey(parentNode);
const parentLevel = this.getNodeLevel(parentNode);
for (let i = parentIndex + 1; i < data.length; i++) {
const node = data[i];
if (node.level <= parentLevel) {
// Reached a sibling or parent level - no more children
break;
}
if (this.isChildOfParent(node, parentKey, parentLevel)) {
childIndices.push(i);
}
}
return childIndices;
}
/**
* Collects parent and children nodes to move
*/
collectNodesToMove(data, parentIndex, childIndices) {
const nodes = [data[parentIndex]];
childIndices.forEach(index => nodes.push(data[index]));
return nodes;
}
/**
* Removes nodes from data array
*/
removeNodesFromData(data, parentIndex, childIndices) {
const indicesToRemove = new Set([parentIndex, ...childIndices]);
return data.filter((_, index) => !indicesToRemove.has(index));
}
/**
* Calculates the adjusted insertion index when moving with children
*/
calculateAdjustedInsertIndex(previousIndex, targetIndex, childIndices) {
if (targetIndex <= previousIndex) {
// Moving up - no adjustment needed
return targetIndex;
}
// Moving down - adjust for removed children
const lastChildIndex = childIndices.length > 0 ? childIndices[childIndices.length - 1] : previousIndex;
// Check if dropping within own children (only when children are visible)
// After parent removal, children shift down by 1, so last child is at lastChildIndex - 1
if (childIndices.length > 0 && targetIndex <= lastChildIndex - 1) {
// Target is within our own children - no real move
return previousIndex;
}
// Adjust for the number of removed children
// currentIndex accounts for parent removal, we need to adjust for children
return targetIndex - childIndices.length;
}
/**
* Calculates the drop position details
*/
calculateDropPosition(context) {
const { draggedNode, currentIndex, data, previousIndex } = context;
const draggedLevel = this.getNodeLevel(draggedNode);
// Create simulated data with item removed
const simulatedData = [...data];
simulatedData.splice(previousIndex, 1);
// Calculate target level
const targetLevel = this.calculateTargetLevel(simulatedData, currentIndex, draggedLevel);
// Calculate target parent path for child nodes
const targetParentPath = draggedLevel > 0 ? this.findTargetParentPath(simulatedData, currentIndex, draggedLevel) : '';
return {
targetLevel,
targetParentPath,
insertIndex: currentIndex
};
}
/**
* Determines the expected level at the target position
*/
calculateTargetLevel(simulatedData, targetIndex, draggedLevel) {
// Handle edge cases
if (targetIndex === 0) {
return 0;
}
if (targetIndex >= simulatedData.length) {
return this.calculateEndPositionLevel(simulatedData, draggedLevel);
}
// Handle middle positions
return this.calculateMiddlePositionLevel(simulatedData, targetIndex, draggedLevel);
}
/**
* Calculates level for end position drops
*/
calculateEndPositionLevel(simulatedData, draggedLevel) {
if (simulatedData.length === 0) {
return 0;
}
const lastItem = simulatedData[simulatedData.length - 1];
// Allow root items to be placed after child groups
if (lastItem.level > 0 && draggedLevel === 0) {
return 0;
}
return lastItem.level;
}
/**
* Calculates level for middle position drops
*/
calculateMiddlePositionLevel(simulatedData, targetIndex, draggedLevel) {
const prevItem = simulatedData[targetIndex - 1];
const nextItem = simulatedData[targetIndex];
// Check for boundary positions
if (this.isBoundaryPosition(prevItem, nextItem)) {
return this.handleBoundaryPosition(prevItem, nextItem, draggedLevel);
}
// Check for parent-first-child position
if (this.isParentFirstChildPosition(prevItem, nextItem)) {
return nextItem.level;
}
// Default to previous item's level
return prevItem?.level || 0;
}
/**
* Finds the parent path for the target position
*/
findTargetParentPath(simulatedData, targetIndex, targetLevel) {
// Look at surrounding items
const prevItem = targetIndex > 0 ? simulatedData[targetIndex - 1] : null;
const nextItem = targetIndex < simulatedData.length ? simulatedData[targetIndex] : null;
// Check if dropping after a parent (becoming first child)
if (this.isDroppingAfterParent(prevItem, targetLevel)) {
return this.getNodeKey(prevItem);
}
// Check siblings for parent path
const siblingParentPath = this.getParentPathFromSiblings(prevItem, nextItem, targetLevel);
if (siblingParentPath) {
return siblingParentPath;
}
// Search backwards for parent
return this.searchBackwardsForParent(simulatedData, targetIndex, targetLevel);
}
getNodeLevel(node) {
return node.level;
}
getNodeKey(node) {
const keyPath = node.property.keyPath;
return keyPath?.join('.') || node.property.name;
}
getParentPath(node, level) {
const keyPath = node.property.keyPath;
return keyPath?.slice(0, level).join('.') || '';
}
isExpandableNode(node) {
return node.expandable;
}
isChildOfParent(node, parentKey, parentLevel) {
const nodeKeyPath = node.property.keyPath;
if (!nodeKeyPath)
return false;
const nodeParentPath = nodeKeyPath.slice(0, parentLevel + 1).join('.');
return nodeParentPath === parentKey;
}
isBoundaryPosition(prevItem, nextItem) {
return !!(prevItem && nextItem && prevItem.level > 0 && nextItem.level === 0);
}
isParentFirstChildPosition(prevItem, nextItem) {
return !!(prevItem?.expandable && nextItem && nextItem.level > prevItem.level);
}
isDroppingAfterParent(prevItem, targetLevel) {
return !!(prevItem?.expandable && prevItem.level === targetLevel - 1);
}
handleBoundaryPosition(prevItem, nextItem, draggedLevel) {
if (draggedLevel === 0) {
return 0;
}
if (draggedLevel === prevItem.level) {
// Additional validation could go here for same-parent check
return prevItem.level;
}
return prevItem.level;
}
getParentPathFromSiblings(prevItem, nextItem, targetLevel) {
if (prevItem?.level === targetLevel) {
return this.getParentPath(prevItem, targetLevel);
}
if (nextItem?.level === targetLevel) {
return this.getParentPath(nextItem, targetLevel);
}
return '';
}
searchBackwardsForParent(data, startIndex, targetLevel) {
for (let i = startIndex - 1; i >= 0; i--) {
const node = data[i];
if (node.level === targetLevel - 1 && node.expandable) {
return this.getNodeKey(node);
}
if (node.level < targetLevel - 1) {
break;
}
}
return '';
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyDragDropService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyDragDropService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyDragDropService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
/**
* Represents a list of asset properties with hierarchical tree structure.
*/
class AssetPropertyListComponent {
get cdkDragDisabled() {
return !!this.filterText;
}
/**
* Constructor initializes reactive effects for expansion modes.
*/
constructor() {
/**
* Reference to the CDdk tree component.
*/
this.tree = viewChild('tree', ...(ngDevMode ? [{ debugName: "tree" }] : []));
/**
* Configuration for the asset property list.
*/
this.config = {};
/**
* Extra properties to be displayed in the list.
*/
this.extraProperties = [];
/**
* Emits the selected properties.
*/
this.selectedProperties = new EventEmitter();
/**
* List of all properties.
*/
this.properties = [];
/**
* Text input for filtering properties.
*/
this.filterText = '';
/**
* Data source for the tree structure.
*/
this.dataSource = new FlatTreeDataSource();
/**
* Map of flat nodes for quick lookup.
*/
this.flatNodeMap = new Map();
/**
* Indicates if all nodes are selected.
*/
this.allSelected = false;
/**
* Indicates if the selection state is indeterminate.
*/
this.indeterminate = false;
this.collapseAllLabel = gettext('Collapse all');
this.expandAllLabel = gettext('Expand all');
this.dragHandleEnabledLabel = gettext('Click and drag to reorder');
this.dragHandleDisabledLabel = gettext('Clear filtering to reorder');
this.TREE_NODE_INDENT = 24;
/**
* Stores all nodes in their complete ordered state (source of truth for order)
*/
this.allNodesComplete = [];
/**
* Tracks which parent nodes are expanded (key = node key, value = expanded state)
*/
this.expansionState = new Map();
/**
* List of properties that have been added temporarily as custom properties.
*/
this.allAddedProperties = [];
/**
* List of properties that have been added temporarily as custom properties filtered according to context asset.
*/
this.displayAddedProperties = [];
/**
* Subject for handling filter input.
*/
this.filterSubject$ = new BehaviorSubject('');
/**
* Subject for handling component destruction.
*/
this.destroy$ = new Subject();
/**
* Service for managing asset properties.
*/
this.assetPropertiesService = inject(AssetPropertiesService);
this.customPropertiesDrawerService = inject(CustomPropertiesDrawerService);
this.computedPropertiesService = inject(ComputedPropertiesService);
this.modalService = inject(BsModalService);
this.dragDropService = inject(AssetPropertyDragDropService);
effect(() => {
if ((this.config.expansionMode === 'nonCollapsible' ||
this.config.expansionMode === 'expandedByDefault') &&
this.tree()) {
queueMicrotask(() => {
this.expandAllNodesInState();
this.rebuildVisibleData();
});
}
});
}
ngOnInit() {
this.config = {
...defaultAssetPropertyListConfig,
...this.config
};
this.filterSubject$
.pipe(takeUntil(this.destroy$), debounceTime(200))
.subscribe(() => this.filterTree());
}
async ngOnChanges(changes) {
if (changes['asset'] && !changes['asset'].firstChange) {
await this.updateDisplayAddedProperties();
}
const shouldRebuild = changes['asset'] || changes['extraProperties'];
await this.updateProperties(!!shouldRebuild);
}
ngAfterViewInit() {
if (this.config.selectMode === 'single') {
this.selectedProperties.next([]);
}
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
/**
* Fetches and categorizes properties.
* @returns A promise resolving to the list of asset properties.
*/
async getProperties() {
if (this.asset) {
return this.categorizeAndFlattenHierarchicalProperties(await this.assetPropertiesService.getInitialProperties(this.asset));
}
else {
if (this.config?.emptyStateContent === 'default-properties') {
return this.categorizeAndFlattenHierarchicalProperties(defaultAssetProperties);
}
else if (Array.isArray(this.config?.emptyStateContent)) {
return this.config.emptyStateContent;
}
}
return [];
}
/**
* Opens the drawer to add new custom properties.
*/
async addProperty() {
const propertiesToAdd = await this.customPropertiesDrawerService.getCustomProperties(this.asset);
if (!propertiesToAdd || propertiesToAdd.length === 0) {
return;
}
const propertiesNeedingConfig = [];
const definitionsNeedingConfig = [];
const propertiesToActuallyAdd = [];
for (const prop of propertiesToAdd) {
if (prop.computed) {
const definition = await this.computedPropertiesService.getByName(prop.name);
if (definition.configComponent || definition.loadConfigComponent) {
propertiesNeedingConfig.push(prop);
definitionsNeedingConfig.push(definition);
}
else {
prop.temporary = true;
propertiesToActuallyAdd.push(prop);
}
}
else {
prop.temporary = true;
propertiesToActuallyAdd.push(prop);
}
}
if (propertiesNeedingConfig.length > 0) {
try {
const configs = await this.configureMultipleProperties(propertiesNeedingConfig, definitionsNeedingConfig);
propertiesNeedingConfig.forEach((prop, index) => {
prop.config = configs[index];
prop.temporary = true;
propertiesToActuallyAdd.push(prop);
});
}
catch { }
}
if (propertiesToActuallyAdd.length === 0) {
return;
}
propertiesToActuallyAdd.forEach(prop => {
if (this.asset?.id) {
prop.contextAssetId = this.asset.id;
}
// Generate instanceId for computed properties to allow multiple instances
if (prop.computed && !prop.instanceId) {
prop.instanceId = crypto.randomUUID();
}
});
this.allAddedProperties = this.allAddedProperties.concat(propertiesToActuallyAdd);
await this.updateDisplayAddedProperties();
await this.updateProperties(false, propertiesToActuallyAdd);
this.selectedProperties.next(this.dataSource.data.filter(n => n.property.active).map(n => n.property));
}
/**
* Displays the configuration modal for multiple computed properties and updates their configurations.
* @param properties Properties to configure.
* @param definitions Computed property definitions including config components.
* @returns Promise resolving to array of configurations.
*/
configureMultipleProperties(properties, definitions) {
const modalRef = this.modalService.show(ComputedPropertiesConfigComponent, {
ignoreBackdropClick: true,
class: 'modal-lg',
ariaDescribedby: 'modal-body',
ariaLabelledBy: 'modal-title',
initialState: {
properties,
definitions,
asset: this.asset
}
}).content;
return modalRef.result;
}
/**
* Displays the configuration modal for a computed property and updates its configuration.
* @param property Property to configure.
* @param definition Computed property definition including config component.
* @returns true if the property was configured, false if cancelled.
*/
async configureProperty(property, definition) {
try {
const configs = await this.configureMultipleProperties([property], [definition]);
property.config = configs[0];
return true;
}
catch (err) {
return false;
}
}
/**
* Removes temporary property from the list.
* @param property The property to remove.
*/
async removeProperty(property) {
let filterFn;
if (this.assetPropertiesService.isComplexProperty(property)) {
filterFn = (p) => {
if (p === property) {
// remove complex property itself
return false;
}
if ('keyPath' in p && Array.isArray(p.keyPath)) {
// remove children of complex property
const keyPath = p.keyPath;
return keyPath[0] !== property.name;
}
return true;
};
}
else {
// Remove only the property itself
filterFn = (p) => p !== property;
}
this.allAddedProperties = this.allAddedProperties.filter(filterFn);
this.displayAddedProperties = this.displayAddedProperties.filter(filterFn);
// Also update the complete data when removing properties
const propertyKey = property.keyPath?.join('.') || property.name;
this.allNodesComplete = this.allNodesComplete.filter(node => {
const nodeKey = node.property.keyPath?.join('.') || node.property.name;
if (this.assetPropertiesService.isComplexProperty(property)) {
// Remove the property and its children
return !nodeKey.startsWith(propertyKey);
}
else {
return nodeKey !== propertyKey;
}
});
this.properties = this.allNodesComplete.map(node => node.property);
await this.updateProperties(false);
}
async editProperty(property) {
const definition = await this.computedPropertiesService.getByName(property.name);
const configured = await this.configureProperty(property, definition);
if (configured && this.asset?.id) {
property.contextAssetId = this.asset.id;
}
await this.updateProperties(false);
this.selectedProperties.next(this.dataSource.data.filter(n => n.property.active).map(n => n.property));
}
hasChild(node) {
return node.expandable;
}
/**
* Checks if the property's context asset matches the current asset.
* @param node The node to check.
* @returns True if the context asset doesn't match the current asset.
*/
isContextMismatch(node) {
if (!node.property.computed || !node.contextAssetId) {
return false;
}
return this.asset?.id !== node.contextAssetId;
}
/**
* Gets the level of a node.
* @param node The node to check.
* @returns The level of the node.
*/
getLevel(node) {
return node.level;
}
/**
* Gets the parent node of a given node.
* @param node The node to check.
* @returns The parent node or null if none exists.
*/
getParentNode(node) {
const keyPath = node.property.keyPath;
if (!keyPath || keyPath.length <= 1) {
return null;
}
const parentPath = keyPath.slice(0, -1).join('.');
return this.flatNodeMap.get(parentPath) || null;
}
/**
* Checks if a node is expanded.
* @param node The node to check.
* @returns True if the node is expanded.
*/
isNodeExpanded(node) {
if (!node.expandable) {
return false;
}
const key = this.getNodeKey(node);
return this.expansionState.get(key) ?? false;
}
/**
* Toggles the expansion state of a node.
* @param node The node to toggle.
*/
toggleNode(node) {
if (!node.expandable) {
return;
}
const key = this.getNodeKey(node);
const currentState = this.expansionState.get(key) ?? false;
this.expansionState.set(key, !currentState);
this.rebuildVisibleData();
this.updateAreAllNodesExpandedState();
}
/**
* Selects or deselects all nodes.
* @param selected True to select all, false to deselect.
*/
selectAll(selected) {
this.allSelected = selected;
this.indeterminate = false;
const visibleNodes = this.dataSource.data.filter(node => node.isVisible);
visibleNodes.forEach(node => {
node.property.active = selected;
});
const selectedProperties = this.dataSource.data
.filter(n => n.property.active)
.map(n => n.property);
this.selectedProperties.next(selectedProperties);
}
/**
* Handles single selection mode.
* @param selected True if the node is selected.
* @param node The node to select.
*/
onSelectSingle(selected, node) {
this.allNodesComplete.forEach(n => {
n.property.active = false;
});
node.property.active = selected;
if (selected && node.expandable && !this.isNodeExpanded(node)) {
const key = this.getNodeKey(node);
this.expansionState.set(key, true);
this.rebuildVisibleData();
this.updateAreAllNodesExpandedState();
}
if (selected) {
this.selectedProperties.next([node.property]);
}
}
/**
* Handles multi-selection mode.
* @param selected True if the node is selected.
* @param node The node to select.
*/
onSelectMulti(selected, node) {
node.property.active = selected;
if (selected && node.expandable && !this.isNodeExpanded(node)) {
const key = this.getNodeKey(node);
this.expansionState.set(key, true);
this.rebuildVisibleData();
this.updateAreAllNodesExpandedState();
}
if (node.expandable) {
this.updateChildSelectionStatus(node, selected);
}
this.updateSelectAllState();
const selectedProperties = this.allNodesComplete
.filter(n => n.property.active)
.map(n => n.property);
this.selectedProperties.next(selectedProperties);
}
/**
* Initiates a filter operation.
*/
onFilter() {
this.filterSubject$.next(this.filterText);
}
/**
* Clears the filter input.
*/
clearFilter() {
this.filterText = '';
this.onFilter();
}
drop(event) {
if (this.cdkDragDisabled || !event.isPointerOverContainer) {
return;
}
const newVisibleData = this.dragDropService.handleDrop({
draggedNode: this.dataSource.data[event.previousIndex],
previousIndex: event.previousIndex,
currentIndex: event.currentIndex,
data: this.dataSource.data
});
if (newVisibleData) {
this.updateCompleteDataOrder(newVisibleData);
this.rebuildVisibleData();
const selectedProperties = this.allNodesComplete
.filter(n => n.property.active)
.map(n => n.property);
this.selectedProperties.next(selectedProperties);
}
}
/**
* Toggles expansion of all nodes - expands all if any are collapsed, otherwise collapses all.
*/
toggleExpandCollapseAll() {
if (this.areAllNodesExpanded) {
this.collapseAllNodesInState();
}
else {
this.expandAllNodesInState();
}
this.rebuildVisibleData();
this.updateAreAllNodesExpandedState();
}
/**
* Calculates the max-width for a column based on node level and configuration.
* @param node The node to calculate width for.
* @returns The calculated max-width CSS value.
*/
getColumnMaxWidth(node) {
const percentage = !this.config.showKey && !this.config.showValue ? '100' : '50';
const levelOffset = (this.getLevel(node) + 1) * this.TREE_NODE_INDENT;
const levelAdjustment = this.getLevel(node) * 12;
return `calc(${percentage}% - ${levelOffset}px + ${levelAdjustment}px)`;
}
/**
* Gets the unique key for a node.
* @param node The node.
* @returns The unique key.
*/
getNodeKey(node) {
const keyPath = node.property.keyPath;
if (keyPath) {
return keyPath.join('.');
}
if (node.property.computed && node.property.instanceId) {
return `${node.property.name}:${node.property.instanceId}`;
}
return node.property.name;
}
/**
* Updates the areAllNodesExpanded state based on expansion state.
*/
updateAreAllNodesExpandedState() {
const parentNodes = this.allNodesComplete.filter(node => node.expandable);
if (parentNodes.length === 0) {
this.areAllNodesExpanded = false;
return;
}
this.areAllNodesExpanded = parentNodes.every(node => {
const key = this.getNodeKey(node);
return this.expansionState.get(key) ?? false;
});
}
/**
* Expands all nodes in the expansion state.
*/
expandAllNodesInState() {
const parentNodes = this.allNodesComplete.filter(node => node.expandable);
parentNodes.forEach(node => {
const key = this.getNodeKey(node);
this.expansionState.set(key, true);
});
this.areAllNodesExpanded = true;
}
/**
* Collapses all nodes in the expansion state.
*/
collapseAllNodesInState() {
const parentNodes = this.allNodesComplete.filter(node => node.expandable);
parentNodes.forEach(node => {
const key = this.getNodeKey(node);
this.expansionState.set(key, false);
});
this.areAllNodesExpanded = false;
}
/**
* Updates the list of properties based on the configuration.
* @param rebuildFromAsset If true, rebuilds from scratch. Otherwise preserves existing order and only adds new properties.
* @param prependProperties New properties to add if not rebuilding from asset.
*/
async updateProperties(rebuildFromAsset = false, prependProperties = []) {
const previouslySelectedProperties = this.dataSource.data
.filter(node => node.property.active)
.map(node => node.property);
if (this.config.inputPropertiesHandle === 'merge' || !this.config.inputPropertiesHandle) {
if (rebuildFromAsset) {
this.properties = [
...prependProperties,
...this.displayAddedProperties,
...this.extraProperties,
...(await this.getProperties())
];
}
else {
this.properties = [...prependProperties, ...this.properties];
}
}
else if (this.config.inputPropertiesHandle === 'override') {
this.properties = [
...prependProperties,
...this.displayAddedProperties,
...this.extraProperties
];
}
this.buildTreeNodes();
if (previouslySelectedProperties.length > 0) {
previouslySelectedProperties.forEach(selectedProp => {
const matchingNode = this.dataSource.data.find(node => this.assetPropertiesService.propertiesMatch(node.property, selectedProp));
if (matchingNode) {
matchingNode.property.active = true;
}
});
this.updateSelectAllState();
}
this.applySelectedPropertiesFromConfig();
}
applySelectedPropertiesFromConfig() {
if (!this.config.selectedProperties || this.config.selectedProperties.length === 0) {
return;
}
this.config.selectedProperties.forEach((configProperty, index) => {
const matchingNode = this.dataSource.data.find(node => this.assetPropertiesService.propertiesMatch(node.property, configProperty));
if (matchingNode) {
if (this.config.selectMode === 'single') {
// For single mode, only select the first property and break
if (index === 0) {
this.onSelectSingle(true, matchingNode);
}
return;
}
else if (this.config.selectMode === 'multi') {
this.onSelectMulti(true, matchingNode);
}
}
});
}
/**
* Updates the display added properties based on compatibility with current asset.
*/
async updateDisplayAddedProperties() {
this.displayAddedProperties = await this.assetPropertiesService.filterCompatibleProperties(this.allAddedProperties, this.asset);
}
/**
* Updates the selection status of child nodes.
* @param parent The parent node.
* @param selected True if the parent is selected.
*/
updateChildSelectionStatus(parent, selected) {
if (!parent.expandable)
return;
const parentKey = parent.property.keyPath?.join('.') || parent.property.name;
this.allNodesComplete.forEach(node => {
const nodePath = node.property.keyPath;
if (nodePath && nodePath.join('.').startsWith(parentKey) && node !== parent) {
node.property.active = selected;
}
});
}
/**
* Checks if a property matches the filter criteria.
* @param property The property to check.
* @param filterText The filter text.
* @returns True if the property matches.
*/
matchesSearch(property, filterText) {
const filterByFields = [
property.label,
property.name,
property.keyPath?.join('.'),
property.title
].filter(Boolean);
return filterByFields.some(field => field?.toLowerCase().includes(filterText));
}
/**
* Updates the selection state for all nodes.
*/
updateSelectAllState() {
if (this.config.selectMode !== 'multi')
return;
const visibleNodes = this.dataSource.data.filter(node => node.isVisible);
const totalCount = visibleNodes.length;
const selectedCount = visibleNodes.filter(node => node.property.active).length;
this.allSelected = totalCount > 0 && selectedCount === totalCount;
this.indeterminate = selectedCount > 0 && selectedCount < totalCount;
// Update indeterminate state for each complex node
this.updateIndeterminateStates();
}
/**
* Builds the tree nodes from the properties.
*/
buildTreeNodes() {
const treeData = [];
this.flatNodeMap.clear();
this.properties.forEach(property => {
const isComplex = this.assetPropertiesService.isComplexProperty(property);
const hasKeyPath = !!property.keyPath;
const level = hasKeyPath ? property.keyPath.length - 1 : 0;
// For computed properties with instanceId, include it in the key to allow multiple instances
let key;
if (hasKeyPath) {
key = property.keyPath.join('.');
}
else if (property.computed && property.instanceId) {
key = `${property.name}:${property.instanceId}`;
}
else {
key = property.name;
}
const node = {
expandable: isComplex,
level,
property,
isVisible: true,
indeterminate: false,
contextAssetId: property.contextAssetId
};
this.flatNodeMap.set(key, node);
treeData.push(node);
});
this.allNodesComplete = [...treeData];
// Apply filter and rebuild visible data
this.filterTree();
this.updateSelectAllState();
this.hasExpandableNodes = this.allNodesComplete.some(node => node.expandable);
if (this.config.expansionMode === 'nonCollapsible' ||
this.config.expansionMode === 'expandedByDefault') {
queueMicrotask(() => {
this.expandAllNodesInState();
this.rebuildVisibleData();
this.updateAreAllNodesExpandedState();
});
}
}
/**
* Update indeterminate state for all complex nodes
*/
updateIndeterminateStates() {
// Only for multi-select mode
if (this.config.selectMode !== 'multi')
return;
// For each complex node, check its children in complete data
this.allNodesComplete.forEach(node => {
if (node.expandable) {
const parentKey = node.property.keyPath?.join('.') || node.property.name;
// Find direct children (level = parent.level + 1, keyPath starts with parentKey)
const children = this.allNodesComplete.filter(child => {
if (child === node)
return false;
const childKeyPath = child.property.keyPath;
if (!childKeyPath)
return false;
// Direct child: one level deeper, and keyPath starts with parentKey
return (childKeyPath.length === node.level + 2 &&
childKeyPath.slice(0, node.level + 1).join('.') === parentKey);
});
if (children.length > 0) {
const selectedCount = children.filter(child => child.property.active).length;
node.indeterminate = selectedCount > 0 && selectedCount < children.length;
}
else {
node.indeterminate = false;
}
}
else {
node.indeterminate = false;
}
});
}
/**
* Filters the tree nodes based on the filter input.
*/
filterTree() {
const filterText = this.filterText?.toLowerCase() || '';
const allNodes = this.allNodesComplete.length > 0
? [...this.allNodesComplete]
: Array.from(this.flatNodeMap.values());
if (!filterText) {
allNodes.forEach(node => (node.isVisible = true));
this.rebuildVisibleData();
this.updateSelectAllState();
return;
}
// First pass: Mark nodes that match the filter criteria
allNodes.forEach(node => {
node.isVisible = this.matchesSearch(node.property, filterText);
});
// Second pass: Make parent nodes of visible nodes also visible
allNodes.forEach(node => {
if (node.isVisible && node.level > 0) {
// Find and mark parent nodes as visible
this.makeParentsVisible(node);
}
});
// When filtering, show all matching nodes regardless of expansion state
const filteredNodes = allNodes.filter(node => node.isVisible);
// Update the data source with filtered nodes (ignoring expansion state during filter)
this.dataSource.data = filteredNodes;
this.updateSelectAllState();
}
/**
* Makes all parent nodes visible.
* @param node The node whose parents should be made visible.
*/
makeParentsVisible(node) {
if (!node || node.level === 0)
return;
const keyPath = node.property.keyPath;
if (!keyPath)
return;
// Navigate up the path and find parents in the complete data
for (let i = 1; i < keyPath.length; i++) {
const parentPath = keyPath.slice(0, i).join('.');
const parent = this.allNodesComplete.find(n => {
const nodeKey = n.property.keyPath?.join('.') || n.property.name;
return nodeKey === parentPath;
});
if (parent) {
parent.isVisible = true;
}
else {
break;
}
}
}
/**
* Rebuilds visible data based on expansion state (only when not filtering).
*/
rebuildVisibleData() {
if (this.filterText) {
return;
}
const visible = [];
for (const node of this.allNodesComplete) {
if (this.shouldNodeBeVisible(node)) {
visible.push(node);
}
}
this.dataSource.data = visible;
this.updateSelectAllState();
}
/**
* Determines if a node should be visible based on expansion state.
* @param node The node to check.
* @returns True if the node should be visible.
*/
shouldNodeBeVisible(node) {
// Root nodes always visible (when not filtered)
if (node.level === 0) {
return node.isVisible;
}
// Check all parents are expanded
const keyPath = node.property.keyPath;
if (!keyPath) {
return node.isVisible;
}
for (let i = 1; i <= node.level; i++) {
const parentPath = keyPath.slice(0, i).join('.');
if (!this.expansionState.get(parentPath)) {
return false;
}
}
return node.isVisible;
}
/**
* Updates complete data order after drag-drop on visible data.
* @param newVisibleData The new order of visible nodes.
*/
updateCompleteDataOrder(newVisibleData) {
// Create a map of visible nodes to their new positions
const visiblePositionMap = new Map();
newVisibleData.forEach((node, index) => {
visiblePositionMap.set(node, index);
});
// Separate nodes into visible and hidden
const visibleNodes = [];
const hiddenNodes = [];
this.allNodesComplete.forEach(node => {
if (visiblePositionMap.has(node)) {
visibleNodes.push(node);
}
else {
hiddenNodes.push(node);
}
});
// Sort visible nodes by their new positions
visibleNodes.sort((a, b) => {
return visiblePositionMap.get(a) - visiblePositionMap.get(b);
});
// Reconstruct complete data: insert hidden children after their parents
const result = [];
for (const visibleNode of visibleNodes) {
result.push(visibleNode);
if (visibleNode.expandable) {
const parentKey = this.getNodeKey(visibleNode);
const children = hiddenNodes.filter(hiddenNode => {
const childKeyPath = hiddenNode.property.keyPath;
if (!childKeyPath)
return false;
const childParentPath = childKeyPath.slice(0, visibleNode.level + 1).join('.');
return childParentPath === parentKey;
});
// Sort children by their original order (by level then by name)
children.sort((a, b) => {
if (a.level !== b.level)
return a.level - b.level;
const aKey = this.getNodeKey(a);
const bKey = this.getNodeKey(b);
return aKey.localeCompare(bKey);
});
result.push(...children);
}
}
this.allNodesComplete = result;
this.properties = result.map(node => node.property);
}
/**
* Categorizes and flattens hierarchical properties.
* @param properties The properties to categorize.
* @returns The flattened list of properties.
*/
categorizeAndFlattenHierarchicalProperties(properties) {
const { computed, simple, complex } = this.assetPropertiesService.categorizeAndFlattenHierarchicalProperties(properties);
return [...computed, ...simple, ...complex];
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: AssetPropertyListComponent, isStandalone: true, selector: "c8y-asset-property-list", inputs: { config: "config", asset: "asset", extraProperties: "extraProperties" }, outputs: { selectedProperties: "selectedProperties" }, queries: [{ propertyName: "assetPropertyAction", first: true, predicate: AssetPropertyActionDirective, descendants: true }], viewQueries: [{ propertyName: "tree", first: true, predicate: ["tree"], descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"tree-container bg-inherit\"\n [attr.aria-label]=\"'Asset property list' | translate\"\n role=\"tree\"\n>\n @if (config.showHeader || config.filterable) {\n <div class=\"select-all-container bg-inherit sticky-top separator-bottom\">\n @if (config.filterable) {\n <div\n class=\"form-group m-b-0 p-16 d-flex\"\n [ngClass]=\"{ 'separator-bottom': config.showHeader }\"\n >\n <div class=\"input-group input-group-search\">\n <input\n class=\"form-control\"\n [attr.aria-label]=\"'Filter properties' | translate\"\n placeholder=\"{{ 'Filter properties' | translate }}\"\n role=\"searchbox\"\n type=\"search\"\n autocomplete=\"off\"\n [(ngModel)]=\"filterText\"\n (input)=\"onFilter()\"\n [disabled]=\"!dataSource.data.length\"\n #filter\n />\n @if (filter.value.length === 0) {\n <span\n class=\"input-group-addon\"\n [attr.aria-hidden]=\"true\"\n >\n <i\n class=\"dlt-c8y-icon-search\"\n [c8yIcon]=\"'search'\"\n ></i>\n </span>\n }\n @if (filter.value.length > 0) {\n <span\n class=\"input-group-addon\"\n [attr.aria-hidden]=\"true\"\n >\n <i\n class=\"text-muted dlt-c8y-icon-times\"\n [c8yIcon]=\"'times'\"\n [attr.aria-label]=\"'Clear filter' | translate\"\n tabindex=\"0\"\n role=\"button\"\n (click)=\"clearFilter()\"\n ></i>\n </span>\n }\n </div>\n </div>\n }\n @if (config.showHeader && dataSource.data.length) {\n <div class=\"d-flex a-i-center overflow-hidden\">\n <div\n class=\"flex-no-shrink d-flex a-i-center\"\n [ngClass]=\"{\n 'p-l-40': !hasExpandableNodes && config.expansionMode !== 'nonCollapsible',\n 'p-r-8': config.selectMode !== 'none' && config.expansionMode === 'nonCollapsible'\n }\"\n >\n @if (config.expansionMode !== 'nonCollapsible' && hasExpandableNodes) {\n @let collapseAllButtonTitle =\n (areAllNodesExpanded ? collapseAllLabel : expandAllLabel) | translate;\n <button\n class=\"collapse-btn btn-dot flex-no-shrink\"\n [title]=\"collapseAllButtonTitle\"\n [attr.aria-label]=\"collapseAllButtonTitle\"\n type=\"button\"\n [ngClass]=\"{ active: areAllNodesExpanded }\"\n (click)=\"toggleExpandCollapseAll()\"\n >\n <i [c8yIcon]=\"'chevron-right'\"></i>\n </button>\n }\n </div>\n @if (config.selectMode === 'multi') {\n <c8y-list-item-checkbox\n class=\"p-l-4 p-r-4\"\n [ngModel]=\"allSelected\"\n [indeterminate]=\"indeterminate\"\n (onSelect)=\"selectAll($event)\"\n ></c8y-list-item-checkbox>\n }\n <div\n class=\"p-t-40\"\n [ngClass]=\"{\n 'p-l-24': config.selectMode === 'single',\n 'p-l-8': config.selectMode === 'none' && config.expansionMode === 'nonCollapsible'\n }\"\n ></div>\n <div class=\"content-flex-30 fit-w m-t-4 m-b-4 m-0\">\n <div class=\"col-6 d-flex a-i-center m-l-0 m-r-0\">\n <span class=\"flex-no-shrink p-l-4 p-r-8\">\n <div class=\"c8y-icon\"></div>\n </span>\n <span class=\"text-medium m-l-8\">\n {{ 'Property' | translate }}\n </span>\n </div>\n @if (config.showKey) {\n <div [ngClass]=\"{ 'col-3': config.showValue, 'col-6': !config.showValue }\">\n <span class=\"text-medium\">{{ 'Key' | translate }}</span>\n </div>\n }\n @if (config.showValue) {\n <div [ngClass]=\"{ 'col-3': config.showKey, 'col-6': !config.showKey }\">\n <span class=\"text-medium\">{{ 'Value' | translate }}</span>\n </div>\n }\n </div>\n @if (assetPropertyAction) {\n <div\n class=\"m-l-8 p-l-24 asset-property-list__actions\"\n [ngClass]=\"{\n 'p-r-40': config.allowAddingCustomProperties,\n 'p-r-8': !config.allowAddingCustomProperties\n }\"\n ></div>\n }\n </div>\n }\n </div>\n }\n\n @if (dataSource.data.length) {\n <cdk-tree\n role=\"presentation\"\n #tree\n [dataSource]=\"dataSource\"\n [levelAccessor]=\"getLevel\"\n cdkDropList\n cdkDropListOrientation=\"vertical\"\n (cdkDropListDropped)=\"drop($event)\"\n [cdkDropListDisabled]=\"!config.allowDragAndDrop || dataSource.data.length < 2\"\n >\n <!-- Tree Node Definition -->\n <cdk-tree-node\n class=\"c8y-list__item--dense\"\n [attr.tabindex]=\"0\"\n [attr.aria-level]=\"getLevel(node) + 1\"\n [attr.aria-expanded]=\"hasChild(node) ? isNodeExpanded(node) : null\"\n [attr.aria-selected]=\"config.selectMode !== 'none' ? node.property.active : null\"\n role=\"treeitem\"\n *cdkTreeNodeDef=\"let node\"\n cdkTreeNodePadding\n [cdkTreeNodePaddingIndent]=\"TREE_NODE_INDENT\"\n [ngClass]=\"{\n nonCollapsible: config.expansionMode === 'nonCollapsible',\n nonSelectable: config.selectMode === 'none'\n }\"\n cdkDrag\n cdkDragLockAxis=\"y\"\n [cdkDragDisabled]=\"cdkDragDisabled\"\n >\n <div class=\"d-flex p-relative overflow-visible bg-inherit fit-h\">\n @if (config.allowDragAndDrop && dataSource.data.length > 1) {\n <div\n class=\"drag-handle-wrapper\"\n [ngClass]=\"{ 'drag-handle-disabled': cdkDragDisabled }\"\n >\n <button\n class=\"drag-handle btn-clean\"\n [title]=\"\n (cdkDragDisabled ? dragHandleDisabledLabel : dragHandleEnabledLabel) | translate\n \"\n [attr.aria-label]=\"\n (cdkDragDisabled ? dragHandleDisabledLabel : dragHandleEnabledLabel) | translate\n \"\n tabindex=\"-1\"\n type=\"button\"\n [disabled]=\"cdkDragDisabled\"\n >\n <i [c8yIcon]=\"'drag-reorder'\"></i>\n </button>\n </div>\n }\n <!-- Toggle Button for expandable nodes -->\n @if (config.expansionMode !== 'nonCollapsible' && hasChild(node)) {\n @let collapseButtonTitle =\n isNodeExpanded(node) | c8yAssetPropertyCollapseButtonTitle: node;\n <button\n class=\"collapse-btn btn-dot flex-no-shrink\"\n [title]=\"collapseButtonTitle\"\n [attr.aria-label]=\"collapseButtonTitle\"\n [attr.aria-expanded]=\"isNodeExpanded(node)\"\n type=\"button\"\n (click)=\"toggleNode(node)\"\n [ngClass]=\"{ active: isNodeExpanded(node) }\"\n >\n <i [c8yIcon]=\"'chevron-right'\"></i>\n </button>\n }\n <!-- Placeholder for non-expandable nodes to maintain alignment -->\n @if (!hasChild(node) || config.expansionMode === 'nonCollapsible') {\n <div\n class=\"flex-no-shrink\"\n [ngClass]=\"{\n 'p-r-40': config.expansionMode !== 'nonCollapsible',\n 'p-r-8': config.expansionMode === 'nonCollapsible'\n }\"\n ></div>\n }\n <!-- Selection Controls -->\n @if (config.selectMode !== 'none') {\n <div class=\"d-contents\">\n @if (config.selectMode === 'single') {\n <c8y-list-item-radio\n class=\"p-l-4 p-r-4\"\n type=\"radio\"\n [selected]=\"node.property.active\"\n (onSelect)=\"onSelectSingle($event, node)\"\n ></c8y-list-item-radio>\n } @else if (config.selectMode === 'multi') {\n <c8y-list-item-checkbox\n class=\"p-l-4 p-r-4\"\n [ngModel]=\"node.property.active\"\n [indeterminate]=\"node.indeterminate\"\n (onSelect)=\"onSelectMulti($event, node)\"\n ></c8y-list-item-checkbox>\n }\n </div>\n }\n <div class=\"content-flex-30 fit-w bg-inherit m-0 min-width-0\">\n <div\n class=\"d-flex a-i-center bg-inherit m-0 flex-grow\"\n [style.max-width]=\"getColumnMaxWidth(node)\"\n >\n <c8y-li-icon\n class=\"p-r-4 p-l-4\"\n [icon]=\"node.property | c8yAssetPropertyIcon\"\n tooltip=\"{{ node.property | c8yAssetPropertyIconTooltip }}\"\n container=\"body\"\n [delay]=\"500\"\n [ngClass]=\"{\n 'p-l-4': config.selectMode !== 'none',\n 'p-l-8': config.selectMode === 'none'\n }\"\n ></c8y-li-icon>\n\n <span class=\"p-r-8 min-width-0 d-flex a-i-center\">\n @if (isContextMismatch(node)) {\n @let tooltipText =\n 'Property configured for a different asset. Reconfigure to match current asset.'\n | translate;\n <button\n class=\"btn-clean m-r-4\"\n [attr.aria-label]=\"tooltipText\"\n [tooltip]=\"tooltipText\"\n placement=\"top\"\n type=\"button\"\n [container]=\"'body'\"\n >\n <i\n class=\"status critical stroked-icon\"\n c8yIcon=\"exclamation-circle\"\n ></i>\n </button>\n }\n\n <div\n class=\"text-truncate\"\n title=\"{{\n node.property.label || node.property.title || node.property.name | translate\n }}\"\n [ngClass]=\"{\n 'text-muted': node.property.temporary,\n 'text-danger': isContextMismatch(node)\n }\"\n >\n {{ node.property.label || node.property.title || node.property.name | translate }}\n </div>\n </span>\n </div>\n @if (config.showKey) {\n <div\n class=\"d-flex a-i-center\"\n [ngClass]=\"{ 'col-3': config.showValue, 'col-6': !config.showValue }\"\n >\n <span\n class=\"d-inline-block tag tag--default a-s-center text-truncate\"\n title=\"{{\n node.property.keyPath?.join('.') || (node.property?.name | translate)\n }}\"\n >\n {{ node.property.keyPath?.join('.') || (node.property?.name | translate) }}\n </span>\n </div>\n }\n @if (asset && config.showValue) {\n <div\n class=\"d-flex a-i-center\"\n [ngClass]=\"{ 'col-3': config.showKey, 'col-6': !config.showKey }\"\n >\n @let value = node.property | c8yAssetPropertyValue: asset | async;\n <span\n class=\"tag tag--info d-inline-block a-s-center text-truncate\"\n title=\"{{ value }}\"\n >\n {{ value }}\n </span>\n </div>\n }\n </div>\n @if (assetPropertyAction || node.property.temporary) {\n <div\n class=\"m-l-8 showOnHover d-flex a-i-center j-c-end asset-property-list__actions\"\n [ngClass]=\"{\n 'p-l-32': !node.property.temporary && config.allowAddingCustomProperties,\n 'has-computed': node.property.computed && node.property.config\n }\"\n >\n @if (node.property.computed && node.property.config) {\n <button\n class=\"btn btn-dot m-l-auto\"\n title=\"{{ 'Configure' | translate }}\"\n (click)=\"editProperty(node.property)\"\n >\n <i [c8yIcon]=\"'cog'\"></i>\n </button>\n }\n @if (node.property.temporary) {\n <button\n class=\"btn btn-dot btn-dot--danger\"\n [attr.aria-label]=\"'Remove' | translate\"\n tooltip=\"{{ 'Remove' | translate }}\"\n container=\"body\"\n type=\"button\"\n [ngClass]=\"{ 'm-l-auto': !node.property.computed || !node.property.config }\"\n [delay]=\"500\"\n (click)=\"removeProperty(node.property)\"\n >\n <i c8yIcon=\"minus-circle\"></i>\n </button>\n }\n <ng-container\n *ngTemplateOutlet=\"\n assetPropertyAction?.template;\n context: {\n $implicit: node.property\n }\n \"\n ></ng-container>\n </div>\n }\n </div>\n </cdk-tree-node>\n </cdk-tree>\n } @else {\n <c8y-ui-empty-state\n icon=\"list\"\n title=\"{{ 'No properties to display' | translate }}\"\n subtitle=\"{{ 'Select an asset to see the available properties.' | translate }}\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n }\n @if (config.allowAddingCustomProperties) {\n <div class=\"sticky-bottom bg-inherit separator-top p-16\">\n <button\n class=\"btn btn-default btn-sm m-l-8\"\n (click)=\"addProperty()\"\n data-cy=\"asset-property-item-add-button\"\n >\n <i [c8yIcon]=\"'plus'\"></i>\n {{ 'Add property' | translate }}\n </button>\n </div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ListGroupModule }, { kind: "component", type: i2.ListItemIconComponent, selector: "c8y-list-item-icon, c8y-li-icon", inputs: ["icon", "status"] }, { kind: "component", type: i2.ListItemCheckboxComponent, selector: "c8y-list-item-checkbox, c8y-li-checkbox", inputs: ["selected", "indeterminate", "disabled", "displayAsSwitch"], outputs: ["onSelect"] }, { kind: "component", type: i2.ListItemRadioComponent, selector: "c8y-list-item-radio, c8y-li-radio", inputs: ["selected", "name", "disabled", "value"], outputs: ["onSelect"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i3.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: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i4.CdkTreeNodeDef, selector: "[cdkTreeNodeDef]", inputs: ["cdkTreeNodeDefWhen"] }, { kind: "directive", type: i4.CdkTreeNodePadding, selector: "[cdkTreeNodePadding]", inputs: ["cdkTreeNodePadding", "cdkTreeNodePaddingIndent"] }, { kind: "component", type: i4.CdkTree, selector: "cdk-tree", inputs: ["dataSource", "treeControl", "levelAccessor", "childrenAccessor", "trackBy", "expansionKey"], exportAs: ["cdkTree"] }, { kind: "directive", type: i4.CdkTreeNode, selector: "cdk-tree-node", inputs: ["role", "isExpandable", "isExpanded", "isDisabled", "cdkTreeNodeTypeaheadLabel"], outputs: ["activation", "expandedChange"], exportAs: ["cdkTreeNode"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "directive", type: 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: 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: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: AssetPropertyValuePipe, name: "c8yAssetPropertyValue" }, { kind: "pipe", type: AssetPropertyIconPipe, name: "c8yAssetPropertyIcon" }, { kind: "pipe", type: AssetPropertyIconTooltipPipe, name: "c8yAssetPropertyIconTooltip" }, { kind: "pipe", type: AssetPropertyCollapseButtonTitlePipe, name: "c8yAssetPropertyCollapseButtonTitle" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertyListComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-asset-property-list', standalone: true, imports: [
NgClass,
AsyncPipe,
FormsModule,
ListGroupModule,
C8yTranslatePipe,
TooltipModule,
IconDirective,
CdkTreeModule,
NgTemplateOutlet,
AssetPropertyValuePipe,
EmptyStateComponent,
AssetPropertyIconPipe,
NgClass,
AssetPropertyIconTooltipPipe,
AssetPropertyCollapseButtonTitlePipe,
CdkDropList,
CdkDrag
], template: "<div\n class=\"tree-container bg-inherit\"\n [attr.aria-label]=\"'Asset property list' | translate\"\n role=\"tree\"\n>\n @if (config.showHeader || config.filterable) {\n <div class=\"select-all-container bg-inherit sticky-top separator-bottom\">\n @if (config.filterable) {\n <div\n class=\"form-group m-b-0 p-16 d-flex\"\n [ngClass]=\"{ 'separator-bottom': config.showHeader }\"\n >\n <div class=\"input-group input-group-search\">\n <input\n class=\"form-control\"\n [attr.aria-label]=\"'Filter properties' | translate\"\n placeholder=\"{{ 'Filter properties' | translate }}\"\n role=\"searchbox\"\n type=\"search\"\n autocomplete=\"off\"\n [(ngModel)]=\"filterText\"\n (input)=\"onFilter()\"\n [disabled]=\"!dataSource.data.length\"\n #filter\n />\n @if (filter.value.length === 0) {\n <span\n class=\"input-group-addon\"\n [attr.aria-hidden]=\"true\"\n >\n <i\n class=\"dlt-c8y-icon-search\"\n [c8yIcon]=\"'search'\"\n ></i>\n </span>\n }\n @if (filter.value.length > 0) {\n <span\n class=\"input-group-addon\"\n [attr.aria-hidden]=\"true\"\n >\n <i\n class=\"text-muted dlt-c8y-icon-times\"\n [c8yIcon]=\"'times'\"\n [attr.aria-label]=\"'Clear filter' | translate\"\n tabindex=\"0\"\n role=\"button\"\n (click)=\"clearFilter()\"\n ></i>\n </span>\n }\n </div>\n </div>\n }\n @if (config.showHeader && dataSource.data.length) {\n <div class=\"d-flex a-i-center overflow-hidden\">\n <div\n class=\"flex-no-shrink d-flex a-i-center\"\n [ngClass]=\"{\n 'p-l-40': !hasExpandableNodes && config.expansionMode !== 'nonCollapsible',\n 'p-r-8': config.selectMode !== 'none' && config.expansionMode === 'nonCollapsible'\n }\"\n >\n @if (config.expansionMode !== 'nonCollapsible' && hasExpandableNodes) {\n @let collapseAllButtonTitle =\n (areAllNodesExpanded ? collapseAllLabel : expandAllLabel) | translate;\n <button\n class=\"collapse-btn btn-dot flex-no-shrink\"\n [title]=\"collapseAllButtonTitle\"\n [attr.aria-label]=\"collapseAllButtonTitle\"\n type=\"button\"\n [ngClass]=\"{ active: areAllNodesExpanded }\"\n (click)=\"toggleExpandCollapseAll()\"\n >\n <i [c8yIcon]=\"'chevron-right'\"></i>\n </button>\n }\n </div>\n @if (config.selectMode === 'multi') {\n <c8y-list-item-checkbox\n class=\"p-l-4 p-r-4\"\n [ngModel]=\"allSelected\"\n [indeterminate]=\"indeterminate\"\n (onSelect)=\"selectAll($event)\"\n ></c8y-list-item-checkbox>\n }\n <div\n class=\"p-t-40\"\n [ngClass]=\"{\n 'p-l-24': config.selectMode === 'single',\n 'p-l-8': config.selectMode === 'none' && config.expansionMode === 'nonCollapsible'\n }\"\n ></div>\n <div class=\"content-flex-30 fit-w m-t-4 m-b-4 m-0\">\n <div class=\"col-6 d-flex a-i-center m-l-0 m-r-0\">\n <span class=\"flex-no-shrink p-l-4 p-r-8\">\n <div class=\"c8y-icon\"></div>\n </span>\n <span class=\"text-medium m-l-8\">\n {{ 'Property' | translate }}\n </span>\n </div>\n @if (config.showKey) {\n <div [ngClass]=\"{ 'col-3': config.showValue, 'col-6': !config.showValue }\">\n <span class=\"text-medium\">{{ 'Key' | translate }}</span>\n </div>\n }\n @if (config.showValue) {\n <div [ngClass]=\"{ 'col-3': config.showKey, 'col-6': !config.showKey }\">\n <span class=\"text-medium\">{{ 'Value' | translate }}</span>\n </div>\n }\n </div>\n @if (assetPropertyAction) {\n <div\n class=\"m-l-8 p-l-24 asset-property-list__actions\"\n [ngClass]=\"{\n 'p-r-40': config.allowAddingCustomProperties,\n 'p-r-8': !config.allowAddingCustomProperties\n }\"\n ></div>\n }\n </div>\n }\n </div>\n }\n\n @if (dataSource.data.length) {\n <cdk-tree\n role=\"presentation\"\n #tree\n [dataSource]=\"dataSource\"\n [levelAccessor]=\"getLevel\"\n cdkDropList\n cdkDropListOrientation=\"vertical\"\n (cdkDropListDropped)=\"drop($event)\"\n [cdkDropListDisabled]=\"!config.allowDragAndDrop || dataSource.data.length < 2\"\n >\n <!-- Tree Node Definition -->\n <cdk-tree-node\n class=\"c8y-list__item--dense\"\n [attr.tabindex]=\"0\"\n [attr.aria-level]=\"getLevel(node) + 1\"\n [attr.aria-expanded]=\"hasChild(node) ? isNodeExpanded(node) : null\"\n [attr.aria-selected]=\"config.selectMode !== 'none' ? node.property.active : null\"\n role=\"treeitem\"\n *cdkTreeNodeDef=\"let node\"\n cdkTreeNodePadding\n [cdkTreeNodePaddingIndent]=\"TREE_NODE_INDENT\"\n [ngClass]=\"{\n nonCollapsible: config.expansionMode === 'nonCollapsible',\n nonSelectable: config.selectMode === 'none'\n }\"\n cdkDrag\n cdkDragLockAxis=\"y\"\n [cdkDragDisabled]=\"cdkDragDisabled\"\n >\n <div class=\"d-flex p-relative overflow-visible bg-inherit fit-h\">\n @if (config.allowDragAndDrop && dataSource.data.length > 1) {\n <div\n class=\"drag-handle-wrapper\"\n [ngClass]=\"{ 'drag-handle-disabled': cdkDragDisabled }\"\n >\n <button\n class=\"drag-handle btn-clean\"\n [title]=\"\n (cdkDragDisabled ? dragHandleDisabledLabel : dragHandleEnabledLabel) | translate\n \"\n [attr.aria-label]=\"\n (cdkDragDisabled ? dragHandleDisabledLabel : dragHandleEnabledLabel) | translate\n \"\n tabindex=\"-1\"\n type=\"button\"\n [disabled]=\"cdkDragDisabled\"\n >\n <i [c8yIcon]=\"'drag-reorder'\"></i>\n </button>\n </div>\n }\n <!-- Toggle Button for expandable nodes -->\n @if (config.expansionMode !== 'nonCollapsible' && hasChild(node)) {\n @let collapseButtonTitle =\n isNodeExpanded(node) | c8yAssetPropertyCollapseButtonTitle: node;\n <button\n class=\"collapse-btn btn-dot flex-no-shrink\"\n [title]=\"collapseButtonTitle\"\n [attr.aria-label]=\"collapseButtonTitle\"\n [attr.aria-expanded]=\"isNodeExpanded(node)\"\n type=\"button\"\n (click)=\"toggleNode(node)\"\n [ngClass]=\"{ active: isNodeExpanded(node) }\"\n >\n <i [c8yIcon]=\"'chevron-right'\"></i>\n </button>\n }\n <!-- Placeholder for non-expandable nodes to maintain alignment -->\n @if (!hasChild(node) || config.expansionMode === 'nonCollapsible') {\n <div\n class=\"flex-no-shrink\"\n [ngClass]=\"{\n 'p-r-40': config.expansionMode !== 'nonCollapsible',\n 'p-r-8': config.expansionMode === 'nonCollapsible'\n }\"\n ></div>\n }\n <!-- Selection Controls -->\n @if (config.selectMode !== 'none') {\n <div class=\"d-contents\">\n @if (config.selectMode === 'single') {\n <c8y-list-item-radio\n class=\"p-l-4 p-r-4\"\n type=\"radio\"\n [selected]=\"node.property.active\"\n (onSelect)=\"onSelectSingle($event, node)\"\n ></c8y-list-item-radio>\n } @else if (config.selectMode === 'multi') {\n <c8y-list-item-checkbox\n class=\"p-l-4 p-r-4\"\n [ngModel]=\"node.property.active\"\n [indeterminate]=\"node.indeterminate\"\n (onSelect)=\"onSelectMulti($event, node)\"\n ></c8y-list-item-checkbox>\n }\n </div>\n }\n <div class=\"content-flex-30 fit-w bg-inherit m-0 min-width-0\">\n <div\n class=\"d-flex a-i-center bg-inherit m-0 flex-grow\"\n [style.max-width]=\"getColumnMaxWidth(node)\"\n >\n <c8y-li-icon\n class=\"p-r-4 p-l-4\"\n [icon]=\"node.property | c8yAssetPropertyIcon\"\n tooltip=\"{{ node.property | c8yAssetPropertyIconTooltip }}\"\n container=\"body\"\n [delay]=\"500\"\n [ngClass]=\"{\n 'p-l-4': config.selectMode !== 'none',\n 'p-l-8': config.selectMode === 'none'\n }\"\n ></c8y-li-icon>\n\n <span class=\"p-r-8 min-width-0 d-flex a-i-center\">\n @if (isContextMismatch(node)) {\n @let tooltipText =\n 'Property configured for a different asset. Reconfigure to match current asset.'\n | translate;\n <button\n class=\"btn-clean m-r-4\"\n [attr.aria-label]=\"tooltipText\"\n [tooltip]=\"tooltipText\"\n placement=\"top\"\n type=\"button\"\n [container]=\"'body'\"\n >\n <i\n class=\"status critical stroked-icon\"\n c8yIcon=\"exclamation-circle\"\n ></i>\n </button>\n }\n\n <div\n class=\"text-truncate\"\n title=\"{{\n node.property.label || node.property.title || node.property.name | translate\n }}\"\n [ngClass]=\"{\n 'text-muted': node.property.temporary,\n 'text-danger': isContextMismatch(node)\n }\"\n >\n {{ node.property.label || node.property.title || node.property.name | translate }}\n </div>\n </span>\n </div>\n @if (config.showKey) {\n <div\n class=\"d-flex a-i-center\"\n [ngClass]=\"{ 'col-3': config.showValue, 'col-6': !config.showValue }\"\n >\n <span\n class=\"d-inline-block tag tag--default a-s-center text-truncate\"\n title=\"{{\n node.property.keyPath?.join('.') || (node.property?.name | translate)\n }}\"\n >\n {{ node.property.keyPath?.join('.') || (node.property?.name | translate) }}\n </span>\n </div>\n }\n @if (asset && config.showValue) {\n <div\n class=\"d-flex a-i-center\"\n [ngClass]=\"{ 'col-3': config.showKey, 'col-6': !config.showKey }\"\n >\n @let value = node.property | c8yAssetPropertyValue: asset | async;\n <span\n class=\"tag tag--info d-inline-block a-s-center text-truncate\"\n title=\"{{ value }}\"\n >\n {{ value }}\n </span>\n </div>\n }\n </div>\n @if (assetPropertyAction || node.property.temporary) {\n <div\n class=\"m-l-8 showOnHover d-flex a-i-center j-c-end asset-property-list__actions\"\n [ngClass]=\"{\n 'p-l-32': !node.property.temporary && config.allowAddingCustomProperties,\n 'has-computed': node.property.computed && node.property.config\n }\"\n >\n @if (node.property.computed && node.property.config) {\n <button\n class=\"btn btn-dot m-l-auto\"\n title=\"{{ 'Configure' | translate }}\"\n (click)=\"editProperty(node.property)\"\n >\n <i [c8yIcon]=\"'cog'\"></i>\n </button>\n }\n @if (node.property.temporary) {\n <button\n class=\"btn btn-dot btn-dot--danger\"\n [attr.aria-label]=\"'Remove' | translate\"\n tooltip=\"{{ 'Remove' | translate }}\"\n container=\"body\"\n type=\"button\"\n [ngClass]=\"{ 'm-l-auto': !node.property.computed || !node.property.config }\"\n [delay]=\"500\"\n (click)=\"removeProperty(node.property)\"\n >\n <i c8yIcon=\"minus-circle\"></i>\n </button>\n }\n <ng-container\n *ngTemplateOutlet=\"\n assetPropertyAction?.template;\n context: {\n $implicit: node.property\n }\n \"\n ></ng-container>\n </div>\n }\n </div>\n </cdk-tree-node>\n </cdk-tree>\n } @else {\n <c8y-ui-empty-state\n icon=\"list\"\n title=\"{{ 'No properties to display' | translate }}\"\n subtitle=\"{{ 'Select an asset to see the available properties.' | translate }}\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n }\n @if (config.allowAddingCustomProperties) {\n <div class=\"sticky-bottom bg-inherit separator-top p-16\">\n <button\n class=\"btn btn-default btn-sm m-l-8\"\n (click)=\"addProperty()\"\n data-cy=\"asset-property-item-add-button\"\n >\n <i [c8yIcon]=\"'plus'\"></i>\n {{ 'Add property' | translate }}\n </button>\n </div>\n }\n</div>\n" }]
}], ctorParameters: () => [], propDecorators: { tree: [{ type: i0.ViewChild, args: ['tree', { isSignal: true }] }], config: [{
type: Input
}], asset: [{
type: Input
}], extraProperties: [{
type: Input
}], selectedProperties: [{
type: Output
}], assetPropertyAction: [{
type: ContentChild,
args: [AssetPropertyActionDirective]
}] } });
/**
* Represents a drawer component for selecting asset properties.
*/
class CustomPropertiesDrawerComponent {
constructor() {
/**
* Title of the drawer.
*/
this.title = gettext('Select property');
/**
* Configuration for the asset property list.
*/
this.config = {
filterable: false,
selectMode: 'multi',
expansionMode: 'collapsedByDefault',
showValue: false,
emptyStateContent: 'default-properties',
allowAddingCustomProperties: false,
inputPropertiesHandle: 'override'
};
/**
* Emits the selected properties when saved.
*/
this.savePropertySelection = new EventEmitter();
/**
* Emits an event when the selection is canceled.
*/
this.cancelPropertySelection = new EventEmitter();
/**
* List of selected properties.
*/
this.selectedProperties = [];
/**
* Reference to the bottom drawer.
*/
this.bottomDrawerRef = inject(BottomDrawerRef);
/**
* Promise resolving to the selected properties.
*/
this.result = new Promise((resolve, reject) => {
this._save = resolve;
this._cancel = reject;
});
this.tabNames = {
regular: gettext('Properties'),
computed: gettext('Computed properties')
};
this.inputText = '';
this.selectedTab = 'regular';
this.regularProperties = [];
this.computedProperties = [];
this.displayedProperties = [];
this.regularCount = 0;
this.computedCount = 0;
this.allComputedProperties = [];
this.searchSubject$ = new BehaviorSubject('');
/**
* Subject for handling component destruction.
*/
this.destroy$ = new Subject();
this.assetPropertiesService = inject(AssetPropertiesService);
this.computedPropertiesService = inject(ComputedPropertiesService);
}
async ngOnInit() {
const computedPropsResult = await this.computedPropertiesService.getByContext(this.asset);
this.allComputedProperties = computedPropsResult.map(({ prop }) => prop);
await this.updateProperties();
this.searchSubject$
.pipe(takeUntil(this.destroy$), debounceTime(350))
.subscribe(() => this.updateProperties());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
/**
* Updates the selected properties.
* @param properties The selected properties.
*/
onSelectedProperties(properties) {
this.selectedProperties = cloneDeep(properties);
}
onTabChange(tabName) {
this.selectedTab = tabName;
this.updateDisplayedProperties();
}
onSearch() {
this.searchSubject$.next(this.inputText);
}
clearSearch() {
this.inputText = '';
this.onSearch();
}
/**
* Saves the selected properties and closes the drawer.
*/
onSave() {
this._save(this.selectedProperties);
this.bottomDrawerRef.close();
}
/**
* Cancels the selection and closes the drawer.
*/
onCancel() {
this._cancel();
this.bottomDrawerRef.close();
}
async loadMore() {
if (this.selectedTab !== 'regular' || !this.paging) {
return;
}
const res = await this.paging.next();
this.paging = res.paging;
const propertiesFromLibrary = [];
const propertiesMOs = res.data;
propertiesMOs.forEach(prop => {
propertiesFromLibrary.push({
c8y_JsonSchema: prop.c8y_JsonSchema,
label: prop.name,
name: Object.keys(prop.c8y_JsonSchema.properties)[0],
type: prop.c8y_JsonSchema.type
});
});
const categorizedNewProps = this.categorizeAndFlattenHierarchicalProperties(propertiesFromLibrary);
const newRegularProps = categorizedNewProps.filter(prop => !prop.computed);
this.regularProperties = [...this.regularProperties, ...newRegularProps];
if (this.selectedTab === 'regular') {
this.displayedProperties = this.regularProperties;
}
}
/**
* Checks if the select button should be disabled.
* @returns True if all selected properties are inactive.
*/
selectIsDisabled() {
return this.selectedProperties?.every(({ active }) => !active);
}
async updateProperties() {
const { propertiesFromLibrary, paging } = await this.assetPropertiesService.getPropertiesFromPropertiesLibrary(this.inputText);
this.paging = paging;
const filteredComputedProperties = this.allComputedProperties.filter(prop => prop.label.toLowerCase().includes(this.inputText?.trim().toLowerCase() || ''));
const filteredDefaultProperties = defaultAssetProperties.filter(prop => prop.label.toLowerCase().includes(this.inputText?.trim().toLowerCase() || ''));
const allProperties = this.categorizeAndFlattenHierarchicalProperties([
...filteredComputedProperties,
...propertiesFromLibrary,
...filteredDefaultProperties
]);
this.regularProperties = allProperties.filter(prop => !prop.computed);
this.computedProperties = allProperties.filter(prop => prop.computed);
this.regularCount = (paging.totalElements || 0) + filteredDefaultProperties.length;
this.computedCount = filteredComputedProperties.length;
this.updateDisplayedProperties();
}
updateDisplayedProperties() {
this.displayedProperties =
this.selectedTab === 'regular' ? this.regularProperties : this.computedProperties;
}
/**
* Categorizes and flattens hierarchical properties.
* @param properties The properties to categorize.
* @returns The flattened list of properties.
*/
categorizeAndFlattenHierarchicalProperties(properties) {
const { computed, simple, complex } = this.assetPropertiesService.categorizeAndFlattenHierarchicalProperties(properties);
return [...computed, ...simple, ...complex];
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: CustomPropertiesDrawerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: CustomPropertiesDrawerComponent, isStandalone: true, selector: "c8y-custom-properties-drawer-component", inputs: { title: "title" }, outputs: { savePropertySelection: "savePropertySelection", cancelPropertySelection: "cancelPropertySelection" }, host: { classAttribute: "d-contents" }, ngImport: i0, template: "<div class=\"card-header separator\">\n <span class=\"h4 card-title\">{{ title | translate }}</span>\n</div>\n\n<div class=\"form-group m-b-8 p-16 d-flex\">\n <div class=\"input-group input-group-search\">\n <input\n class=\"form-control\"\n [attr.aria-label]=\"'Search properties' | translate\"\n placeholder=\"{{ 'Search properties' | translate }}\"\n role=\"searchbox\"\n type=\"search\"\n autocomplete=\"off\"\n [(ngModel)]=\"inputText\"\n (input)=\"onSearch()\"\n #filter\n />\n <span\n class=\"input-group-addon\"\n [attr.aria-hidden]=\"true\"\n *ngIf=\"filter.value.length === 0\"\n >\n <i\n class=\"dlt-c8y-icon-search\"\n [c8yIcon]=\"'search'\"\n ></i>\n </span>\n <span\n class=\"input-group-addon\"\n [attr.aria-hidden]=\"true\"\n *ngIf=\"filter.value.length > 0\"\n >\n <i\n class=\"text-muted dlt-c8y-icon-times\"\n [c8yIcon]=\"'times'\"\n [attr.aria-label]=\"'Clear search' | translate\"\n tabindex=\"0\"\n role=\"button\"\n (click)=\"clearSearch()\"\n ></i>\n </span>\n </div>\n</div>\n\n<c8y-tabs-outlet\n class=\"elevation-none m-b-16\"\n outletName=\"assetPropertiesDrawerTabs\"\n orientation=\"horizontal\"\n></c8y-tabs-outlet>\n\n<c8y-tab\n [isActive]=\"selectedTab === 'regular'\"\n [tabsOutlet]=\"'assetPropertiesDrawerTabs'\"\n [priority]=\"1000\"\n (onSelect)=\"onTabChange('regular')\"\n>\n <i [c8yIcon]=\"'c8y-css'\"></i>\n <span>\n {{ tabNames.regular | translate }}\n <span aria-live=\"assertive\" class=\"m-l-4 badge badge-default flex-no-shrink\">\n {{ regularCount }}\n </span>\n </span>\n</c8y-tab>\n\n<c8y-tab\n [isActive]=\"selectedTab === 'computed'\"\n [tabsOutlet]=\"'assetPropertiesDrawerTabs'\"\n [priority]=\"900\"\n (onSelect)=\"onTabChange('computed')\"\n>\n <i [c8yIcon]=\"'bolt'\"></i>\n <span>\n {{ tabNames.computed | translate }}\n <span class=\"m-l-4 badge badge-default flex-no-shrink\" aria-live=\"assertive\">\n {{ computedCount }}\n </span>\n </span>\n</c8y-tab>\n\n<div class=\"inner-scroll flex-grow\">\n <c8y-asset-property-list\n class=\"bg-component\"\n [config]=\"config\"\n [extraProperties]=\"displayedProperties\"\n (selectedProperties)=\"onSelectedProperties($event)\"\n ></c8y-asset-property-list>\n <c8y-load-more\n *ngIf=\"selectedTab === 'regular'\"\n [paging]=\"paging\"\n [useIntersection]=\"true\"\n (onLoad)=\"loadMore()\"\n ></c8y-load-more>\n</div>\n\n<div class=\"card-footer text-center p-24 separator flex-no-shrink\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n (click)=\"onCancel()\"\n >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Select' | translate }}\"\n type=\"button\"\n [disabled]=\"selectIsDisabled()\"\n (click)=\"onSave()\"\n >\n {{ 'Select' | translate }}\n </button>\n</div>\n", dependencies: [{ kind: "ngmodule", type: FormsModule$1 }, { kind: "directive", type: i1$1.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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: AssetPropertyListComponent, selector: "c8y-asset-property-list", inputs: ["config", "asset", "extraProperties"], outputs: ["selectedProperties"] }, { kind: "component", type: LoadMoreComponent, selector: "c8y-load-more", inputs: ["paging", "useIntersection", "hidden", "container", "class", "maxIterations", "noMoreDataHint", "loadingTemplate", "hideNoMoreDataHint", "loadNextLabel", "loadingLabel"], outputs: ["onLoad"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: TabComponent, selector: "c8y-tab", inputs: ["path", "label", "icon", "priority", "orientation", "injector", "tabsOutlet", "isActive", "text", "showAlways"], outputs: ["onSelect"] }, { kind: "component", type: TabsOutletComponent, selector: "c8y-tabs-outlet,c8y-ui-tabs", inputs: ["tabs", "orientation", "navigatorOpen", "outletName", "context", "openFirstTab", "hasHeader"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: CustomPropertiesDrawerComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-custom-properties-drawer-component', host: {
class: 'd-contents'
}, standalone: true, imports: [
C8yTranslatePipe,
FormsModule$1,
AssetPropertyListComponent,
LoadMoreComponent,
IconDirective,
NgIf,
TabComponent,
TabsOutletComponent
], template: "<div class=\"card-header separator\">\n <span class=\"h4 card-title\">{{ title | translate }}</span>\n</div>\n\n<div class=\"form-group m-b-8 p-16 d-flex\">\n <div class=\"input-group input-group-search\">\n <input\n class=\"form-control\"\n [attr.aria-label]=\"'Search properties' | translate\"\n placeholder=\"{{ 'Search properties' | translate }}\"\n role=\"searchbox\"\n type=\"search\"\n autocomplete=\"off\"\n [(ngModel)]=\"inputText\"\n (input)=\"onSearch()\"\n #filter\n />\n <span\n class=\"input-group-addon\"\n [attr.aria-hidden]=\"true\"\n *ngIf=\"filter.value.length === 0\"\n >\n <i\n class=\"dlt-c8y-icon-search\"\n [c8yIcon]=\"'search'\"\n ></i>\n </span>\n <span\n class=\"input-group-addon\"\n [attr.aria-hidden]=\"true\"\n *ngIf=\"filter.value.length > 0\"\n >\n <i\n class=\"text-muted dlt-c8y-icon-times\"\n [c8yIcon]=\"'times'\"\n [attr.aria-label]=\"'Clear search' | translate\"\n tabindex=\"0\"\n role=\"button\"\n (click)=\"clearSearch()\"\n ></i>\n </span>\n </div>\n</div>\n\n<c8y-tabs-outlet\n class=\"elevation-none m-b-16\"\n outletName=\"assetPropertiesDrawerTabs\"\n orientation=\"horizontal\"\n></c8y-tabs-outlet>\n\n<c8y-tab\n [isActive]=\"selectedTab === 'regular'\"\n [tabsOutlet]=\"'assetPropertiesDrawerTabs'\"\n [priority]=\"1000\"\n (onSelect)=\"onTabChange('regular')\"\n>\n <i [c8yIcon]=\"'c8y-css'\"></i>\n <span>\n {{ tabNames.regular | translate }}\n <span aria-live=\"assertive\" class=\"m-l-4 badge badge-default flex-no-shrink\">\n {{ regularCount }}\n </span>\n </span>\n</c8y-tab>\n\n<c8y-tab\n [isActive]=\"selectedTab === 'computed'\"\n [tabsOutlet]=\"'assetPropertiesDrawerTabs'\"\n [priority]=\"900\"\n (onSelect)=\"onTabChange('computed')\"\n>\n <i [c8yIcon]=\"'bolt'\"></i>\n <span>\n {{ tabNames.computed | translate }}\n <span class=\"m-l-4 badge badge-default flex-no-shrink\" aria-live=\"assertive\">\n {{ computedCount }}\n </span>\n </span>\n</c8y-tab>\n\n<div class=\"inner-scroll flex-grow\">\n <c8y-asset-property-list\n class=\"bg-component\"\n [config]=\"config\"\n [extraProperties]=\"displayedProperties\"\n (selectedProperties)=\"onSelectedProperties($event)\"\n ></c8y-asset-property-list>\n <c8y-load-more\n *ngIf=\"selectedTab === 'regular'\"\n [paging]=\"paging\"\n [useIntersection]=\"true\"\n (onLoad)=\"loadMore()\"\n ></c8y-load-more>\n</div>\n\n<div class=\"card-footer text-center p-24 separator flex-no-shrink\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n (click)=\"onCancel()\"\n >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Select' | translate }}\"\n type=\"button\"\n [disabled]=\"selectIsDisabled()\"\n (click)=\"onSave()\"\n >\n {{ 'Select' | translate }}\n </button>\n</div>\n" }]
}], propDecorators: { title: [{
type: Input
}], savePropertySelection: [{
type: Output
}], cancelPropertySelection: [{
type: Output
}] } });
/**
* Service to open a drawer for selecting custom asset properties.
*/
class CustomPropertiesDrawerService {
constructor() {
this.bottomDrawerService = inject(BottomDrawerService);
}
async getCustomProperties(asset) {
const drawer = this.bottomDrawerService.openDrawer(CustomPropertiesDrawerComponent, {
disableClickOutside: true,
initialState: {
asset
}
});
try {
const resultOf = await drawer.instance.result;
return resultOf;
}
catch (ex) {
return [];
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: CustomPropertiesDrawerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: CustomPropertiesDrawerService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: CustomPropertiesDrawerService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
/**
* Represents a drawer component for selecting asset properties.
*/
class AssetPropertySelectorDrawerComponent {
constructor() {
/**
* Title of the drawer.
*/
this.title = gettext('Select property');
/**
* Extra properties to be displayed in the list.
*/
this.extraProperties = [];
/**
* Emits the selected properties when saved.
*/
this.savePropertySelection = new EventEmitter();
/**
* Emits an event when the selection is canceled.
*/
this.cancelPropertySelection = new EventEmitter();
/**
* List of selected properties.
*/
this.selectedProperties = [];
/**
* Reference to the bottom drawer.
*/
this.bottomDrawerRef = inject(BottomDrawerRef);
/**
* Promise resolving to the selected properties.
*/
this.result = new Promise((resolve, reject) => {
this._save = resolve;
this._cancel = reject;
});
}
/**
* Updates the selected properties.
* @param properties The selected properties.
*/
onSelectedProperties(properties) {
this.selectedProperties = cloneDeep(properties);
}
/**
* Saves the selected properties and closes the drawer.
*/
onSave() {
this._save(this.selectedProperties);
this.bottomDrawerRef.close();
}
/**
* Cancels the selection and closes the drawer.
*/
onCancel() {
this._cancel();
this.bottomDrawerRef.close();
}
/**
* Checks if the select button should be disabled.
* @returns True if all selected properties are inactive.
*/
selectIsDisabled() {
return this.selectedProperties?.every(({ active }) => !active);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertySelectorDrawerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: AssetPropertySelectorDrawerComponent, isStandalone: true, selector: "c8y-asset-property-selector-drawer-component", inputs: { title: "title" }, outputs: { savePropertySelection: "savePropertySelection", cancelPropertySelection: "cancelPropertySelection" }, host: { classAttribute: "d-contents" }, ngImport: i0, template: "<div class=\"card-header separator\">\n <span class=\"h4 card-title\">{{ title | translate }}</span>\n</div>\n\n<div class=\"inner-scroll flex-grow\">\n <c8y-asset-property-list\n class=\"bg-component\"\n [asset]=\"asset\"\n [config]=\"config\"\n [extraProperties]=\"extraProperties\"\n (selectedProperties)=\"onSelectedProperties($event)\"\n ></c8y-asset-property-list>\n</div>\n\n<div class=\"card-footer text-center p-24 separator flex-no-shrink\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n (click)=\"onCancel()\"\n >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Select' | translate }}\"\n type=\"button\"\n [disabled]=\"selectIsDisabled()\"\n (click)=\"onSave()\"\n >\n {{ 'Select' | translate }}\n </button>\n</div>\n", dependencies: [{ kind: "ngmodule", type: FormsModule$1 }, { kind: "component", type: AssetPropertyListComponent, selector: "c8y-asset-property-list", inputs: ["config", "asset", "extraProperties"], outputs: ["selectedProperties"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AssetPropertySelectorDrawerComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-asset-property-selector-drawer-component', host: {
class: 'd-contents'
}, standalone: true, imports: [C8yTranslatePipe, FormsModule$1, AssetPropertyListComponent], template: "<div class=\"card-header separator\">\n <span class=\"h4 card-title\">{{ title | translate }}</span>\n</div>\n\n<div class=\"inner-scroll flex-grow\">\n <c8y-asset-property-list\n class=\"bg-component\"\n [asset]=\"asset\"\n [config]=\"config\"\n [extraProperties]=\"extraProperties\"\n (selectedProperties)=\"onSelectedProperties($event)\"\n ></c8y-asset-property-list>\n</div>\n\n<div class=\"card-footer text-center p-24 separator flex-no-shrink\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n (click)=\"onCancel()\"\n >\n {{ 'Cancel' | translate }}\n </button>\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Select' | translate }}\"\n type=\"button\"\n [disabled]=\"selectIsDisabled()\"\n (click)=\"onSave()\"\n >\n {{ 'Select' | translate }}\n </button>\n</div>\n" }]
}], propDecorators: { title: [{
type: Input
}], savePropertySelection: [{
type: Output
}], cancelPropertySelection: [{
type: Output
}] } });
/**
* Generated bundle index. Do not edit.
*/
export { AssetPropertiesService, AssetPropertyActionDirective, AssetPropertyListComponent, AssetPropertySelectorDrawerComponent, ComputedPropertiesService, CustomPropertiesDrawerService, HOOK_COMPUTED_PROPERTY, RESULT_TYPES, defaultAssetProperties, defaultAssetPropertyListConfig, deviceAssetProperties, hookComputedProperty };
//# sourceMappingURL=c8y-ngx-components-asset-properties.mjs.map