asset-properties/asset-property-list/asset-property-list.component.ts
Represents a list of asset properties with hierarchical tree structure.
| selector | c8y-asset-property-list |
||||||||
| standalone | true |
||||||||
| imports |
C8yTranslatePipe
TooltipModule
CdkTreeModule
AssetPropertyIconPipe
AssetPropertyIconTooltipPipe
AssetPropertyCollapseButtonTitlePipe
|
||||||||
| templateVariables |
|
||||||||
| templateUrl | ./asset-property-list.component.html |
NgClass
AsyncPipe
FormsModule
ListGroupModule
C8yTranslatePipe
TooltipModule
IconDirective
CdkTreeModule
NgTemplateOutlet
AssetPropertyValuePipe
EmptyStateComponent
AssetPropertyIconPipe
AssetPropertyIconTooltipPipe
AssetPropertyCollapseButtonTitlePipe
CdkDropList
CdkDrag
OnInit
OnChanges
OnDestroy
<div
class="tree-container bg-inherit"
[attr.aria-label]="'Asset property list' | translate"
role="tree"
>
@if (config.showHeader || config.filterable) {
<div
class="select-all-container bg-inherit sticky-top"
[ngClass]="{
'separator-bottom': config.filterable || (config.showHeader && dataSource.data.length)
}"
>
@if (config.filterable) {
<div
class="form-group m-b-0 p-16 d-flex"
[ngClass]="{ 'separator-bottom': config.showHeader }"
>
<div class="input-group input-group-search">
<input
class="form-control"
[attr.aria-label]="'Filter properties' | translate"
placeholder="{{ 'Filter properties' | translate }}"
role="searchbox"
type="search"
autocomplete="off"
[(ngModel)]="filterText"
(input)="onFilter()"
[disabled]="!dataSource.data.length"
#filter
/>
@if (filter.value.length === 0) {
<span
class="input-group-addon"
[attr.aria-hidden]="true"
>
<i
class="dlt-c8y-icon-search"
[c8yIcon]="'search'"
></i>
</span>
}
@if (filter.value.length > 0) {
<span
class="input-group-addon"
[attr.aria-hidden]="true"
>
<i
class="text-muted dlt-c8y-icon-times"
[c8yIcon]="'times'"
[attr.aria-label]="'Clear filter' | translate"
tabindex="0"
role="button"
(click)="clearFilter()"
></i>
</span>
}
</div>
</div>
}
@if (config.showHeader && dataSource.data.length) {
<div class="d-flex a-i-center overflow-hidden">
<div
class="flex-no-shrink d-flex a-i-center"
[ngClass]="{
'p-l-40': !hasExpandableNodes && config.expansionMode !== 'nonCollapsible',
'p-r-8': config.selectMode !== 'none' && config.expansionMode === 'nonCollapsible'
}"
>
@if (config.expansionMode !== 'nonCollapsible' && hasExpandableNodes) {
@let collapseAllButtonTitle =
(areAllNodesExpanded ? collapseAllLabel : expandAllLabel) | translate;
<button
class="collapse-btn btn-dot flex-no-shrink"
[title]="collapseAllButtonTitle"
[attr.aria-label]="collapseAllButtonTitle"
type="button"
[ngClass]="{ active: areAllNodesExpanded }"
(click)="toggleExpandCollapseAll()"
>
<i [c8yIcon]="'chevron-right'"></i>
</button>
}
</div>
@if (config.selectMode === 'multi') {
<c8y-list-item-checkbox
class="p-l-4 p-r-4"
[ngModel]="allSelected"
[indeterminate]="indeterminate"
(onSelect)="selectAll($event)"
></c8y-list-item-checkbox>
}
<div
class="p-t-40"
[ngClass]="{
'p-l-24': config.selectMode === 'single',
'p-l-8': config.selectMode === 'none' && config.expansionMode === 'nonCollapsible'
}"
></div>
<div class="content-flex-30 fit-w m-t-4 m-b-4 m-0">
<div class="col-6 d-flex a-i-center m-l-0 m-r-0">
<span class="flex-no-shrink p-l-4 p-r-8">
<div class="c8y-icon"></div>
</span>
<span class="text-medium m-l-8">
{{ 'Property' | translate }}
</span>
</div>
@if (config.showKey) {
<div [ngClass]="{ 'col-3': config.showValue, 'col-6': !config.showValue }">
<span class="text-medium">{{ 'Key' | translate }}</span>
</div>
}
@if (config.showValue) {
<div [ngClass]="{ 'col-3': config.showKey, 'col-6': !config.showKey }">
<span class="text-medium">{{ 'Value' | translate }}</span>
</div>
}
</div>
@if (assetPropertyAction || config.allowAddingCustomProperties) {
<div
class="m-l-8 p-l-24 asset-property-list__actions"
[ngClass]="{
'p-r-40': config.allowAddingCustomProperties,
'p-r-8': !config.allowAddingCustomProperties
}"
></div>
}
</div>
}
</div>
}
@if (dataSource.data.length) {
<cdk-tree
role="presentation"
#tree
[dataSource]="dataSource"
[levelAccessor]="getLevel"
cdkDropList
cdkDropListOrientation="vertical"
(cdkDropListDropped)="drop($event)"
[cdkDropListDisabled]="!config.allowDragAndDrop || dataSource.data.length < 2"
>
<!-- Tree Node Definition -->
<cdk-tree-node
class="c8y-list__item--dense"
[attr.tabindex]="0"
[attr.aria-level]="getLevel(node) + 1"
[attr.aria-expanded]="hasChild(node) ? isNodeExpanded(node) : null"
[attr.aria-selected]="config.selectMode !== 'none' ? node.property.active : null"
role="treeitem"
*cdkTreeNodeDef="let node"
cdkTreeNodePadding
[cdkTreeNodePaddingIndent]="TREE_NODE_INDENT"
[ngClass]="{
nonCollapsible: config.expansionMode === 'nonCollapsible',
nonSelectable: config.selectMode === 'none'
}"
cdkDrag
cdkDragLockAxis="y"
[cdkDragDisabled]="cdkDragDisabled"
>
<div class="d-flex p-relative overflow-visible bg-inherit fit-h">
@if (config.allowDragAndDrop && dataSource.data.length > 1) {
<div
class="drag-handle-wrapper"
[ngClass]="{ 'drag-handle-disabled': cdkDragDisabled }"
>
<button
class="drag-handle btn-clean"
[title]="
(cdkDragDisabled ? dragHandleDisabledLabel : dragHandleEnabledLabel) | translate
"
[attr.aria-label]="
(cdkDragDisabled ? dragHandleDisabledLabel : dragHandleEnabledLabel) | translate
"
tabindex="-1"
type="button"
[disabled]="cdkDragDisabled"
>
<i [c8yIcon]="'drag-reorder'"></i>
</button>
</div>
}
<!-- Toggle Button for expandable nodes -->
@if (config.expansionMode !== 'nonCollapsible' && hasChild(node)) {
@let collapseButtonTitle =
isNodeExpanded(node) | c8yAssetPropertyCollapseButtonTitle: node;
<button
class="collapse-btn btn-dot flex-no-shrink"
[title]="collapseButtonTitle"
[attr.aria-label]="collapseButtonTitle"
[attr.aria-expanded]="isNodeExpanded(node)"
type="button"
(click)="toggleNode(node)"
[ngClass]="{ active: isNodeExpanded(node) }"
>
<i [c8yIcon]="'chevron-right'"></i>
</button>
}
<!-- Placeholder for non-expandable nodes to maintain alignment -->
@if (!hasChild(node) || config.expansionMode === 'nonCollapsible') {
<div
class="flex-no-shrink"
[ngClass]="{
'p-r-40': config.expansionMode !== 'nonCollapsible',
'p-r-8': config.expansionMode === 'nonCollapsible'
}"
></div>
}
<!-- Selection Controls -->
@if (config.selectMode !== 'none') {
<div class="d-contents">
@if (config.selectMode === 'single') {
<c8y-list-item-radio
class="p-l-4 p-r-4"
type="radio"
[selected]="node.property.active"
(onSelect)="onSelectSingle($event, node)"
></c8y-list-item-radio>
} @else if (config.selectMode === 'multi') {
<c8y-list-item-checkbox
class="p-l-4 p-r-4"
[ngModel]="node.property.active"
[indeterminate]="node.indeterminate"
(onSelect)="onSelectMulti($event, node)"
></c8y-list-item-checkbox>
} @else if (config.selectMode === 'plus') {
@if (node.property.active) {
<button
class="btn btn-dot btn-dot--danger m-l-4 m-r-4 p-relative a-s-center"
style="z-index: 5"
[attr.aria-label]="'Remove' | translate"
[tooltip]="'Remove from selected properties' | translate"
type="button"
[delay]="500"
(click)="onSelectMulti(false, node)"
>
<i
class="icon-20"
c8yIcon="minus-circle"
></i>
</button>
}
@if (!node.property.active) {
<button
class="btn btn-dot text-primary m-l-4 m-r-4 p-relative a-s-center"
style="z-index: 5"
[attr.aria-label]="'Add to selected properties' | translate"
[tooltip]="
(node.property.config
? 'Add to selected properties (requires configuration)'
: 'Add to selected properties'
) | translate
"
container="body"
type="button"
[delay]="500"
(click)="onSelectMulti(true, node)"
>
<i
class="text-primary icon-20"
[c8yIcon]="node.property.config ? 'plus-circle-o' : 'plus-circle'"
></i>
</button>
}
}
</div>
}
<div class="content-flex-30 fit-w bg-inherit m-0 min-width-0">
<div
class="d-flex a-i-center bg-inherit m-0 flex-grow"
[style.max-width]="getColumnMaxWidth(node)"
>
<c8y-li-icon
class="p-r-4 p-l-4"
[icon]="node.property | c8yAssetPropertyIcon"
tooltip="{{ node.property | c8yAssetPropertyIconTooltip }}"
container="body"
[delay]="500"
[ngClass]="{
'p-l-4': config.selectMode !== 'none',
'p-l-8': config.selectMode === 'none'
}"
></c8y-li-icon>
<span class="p-r-8 min-width-0 d-flex a-i-center">
@if (isContextMismatch(node)) {
@let tooltipText =
'Property configured for a different asset. Reconfigure to match current asset.'
| translate;
<button
class="btn-clean m-r-4"
[attr.aria-label]="tooltipText"
[tooltip]="tooltipText"
placement="top"
type="button"
[container]="'body'"
>
<i
class="status critical stroked-icon"
c8yIcon="exclamation-circle"
></i>
</button>
}
<div
class="text-truncate"
title="{{
node.property.label || node.property.title || node.property.name | translate
}}"
[ngClass]="{
'text-muted': node.property.temporary,
'text-danger': isContextMismatch(node)
}"
>
{{ node.property.label || node.property.title || node.property.name | translate }}
</div>
</span>
</div>
@if (config.showKey) {
<div
class="d-flex a-i-center"
[ngClass]="{ 'col-3': config.showValue, 'col-6': !config.showValue }"
>
<span
class="d-inline-block tag tag--default a-s-center text-truncate"
title="{{ node.property.keyPath?.join('.') || node.property?.name }}"
>
{{ node.property.keyPath?.join('.') || node.property?.name }}
</span>
</div>
}
@if (asset && config.showValue) {
<div
class="d-flex a-i-center"
[ngClass]="{ 'col-3': config.showKey, 'col-6': !config.showKey }"
>
@let value = node.property | c8yAssetPropertyValue: asset | async;
<span
class="tag tag--info d-inline-block a-s-center text-truncate"
title="{{ value }}"
>
{{ value }}
</span>
</div>
}
</div>
@if (assetPropertyAction || config.allowAddingCustomProperties) {
<div
class="m-l-8 showOnHover d-flex a-i-center j-c-end asset-property-list__actions"
[ngClass]="{
'p-l-24 p-r-40':
!node.property.temporary &&
config.allowAddingCustomProperties &&
!assetPropertyAction,
'p-l-32':
node.property.temporary &&
config.allowAddingCustomProperties &&
!node.property.computed,
'has-computed': node.property.computed && node.property.config
}"
>
@if (node.property.computed && node.property.config) {
<button
class="btn btn-dot m-l-auto"
title="{{ 'Configure' | translate }}"
(click)="editProperty(node.property)"
>
<i [c8yIcon]="'cog'"></i>
</button>
}
@if (node.property.temporary) {
<button
class="btn btn-dot btn-dot--danger"
[attr.aria-label]="'Remove' | translate"
tooltip="{{ 'Remove' | translate }}"
container="body"
type="button"
[ngClass]="{ 'm-l-auto': !node.property.computed || !node.property.config }"
[delay]="500"
(click)="removeProperty(node.property)"
>
<i c8yIcon="minus-circle"></i>
</button>
}
<ng-container
*ngTemplateOutlet="
assetPropertyAction?.template;
context: {
$implicit: node.property
}
"
></ng-container>
</div>
}
</div>
</cdk-tree-node>
</cdk-tree>
} @else {
<c8y-ui-empty-state
icon="list"
title="{{ 'No properties to display' | translate }}"
subtitle="{{ 'Select an asset to see the available properties.' | translate }}"
[horizontal]="true"
></c8y-ui-empty-state>
}
@if (config.allowAddingCustomProperties) {
<div class="sticky-bottom bg-inherit separator-top p-16">
<button
class="btn btn-default btn-sm m-l-8"
(click)="addProperty()"
data-cy="asset-property-item-add-button"
>
<i [c8yIcon]="'plus'"></i>
{{ 'Add property' | translate }}
</button>
</div>
}
</div>