asset-properties/asset-property-list/asset-property-list.component.ts

Description

Represents a list of asset properties with hierarchical tree structure.

Implements

OnInit OnChanges OnDestroy

Example

Metadata

Relationships

Depends on

<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>

results matching ""

    No results matching ""