From da86dcc23404503f91d1dad337b082cf791770ec Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 10 Jun 2026 17:59:39 +0300 Subject: [PATCH 1/3] refactor(iot-hub): centralise item-type icons and chip colors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add itemTypeIcons + getItemTypeIcon helper to iot-hub-item.models, mirroring mp-item.models in the iot-hub project: widgets / dashboard / apps / mdi:function-variant / settings_ethernet / mdi:bell-cog / devices_other with `category` as the fallback. - Replace every hand-rolled item-type → icon switch with calls to getItemTypeIcon: item detail dialog (getTypeIcon / getCompactIcon fallback path), installed-items table (getItemTypeIcon), iot-hub-home (getCompactIcon / getItemTypeIcon), item-card (getPlaceholderIcon default + per-type fallbacks), item-link-card (getCompactIcon / getTypeIcon). - Swap the remaining mat-icon placeholders that render an item-type icon to tb-icon (installed-items table chip, home search popup thumb placeholder, item-link-card thumb fallback). Generic status / action icons stay on mat-icon. - Refresh the colored item-type chip palette in the installed items table to match itemTypeChipColors from mp-item.models, and add the previously-missing tb-type-alarm-rule rule so that chip picks up the new orange palette instead of falling through to no background. --- ...t-hub-installed-items-table.component.html | 2 +- ...t-hub-installed-items-table.component.scss | 14 ++++++----- ...iot-hub-installed-items-table.component.ts | 13 ++-------- .../iot-hub/iot-hub-item-card.component.ts | 22 ++++++++++------- .../iot-hub-item-detail-dialog.component.ts | 19 ++++----------- .../iot-hub-item-link-card.component.html | 2 +- .../iot-hub-item-link-card.component.ts | 24 +++---------------- .../pages/iot-hub/iot-hub-home.component.html | 2 +- .../pages/iot-hub/iot-hub-home.component.ts | 21 +++------------- .../models/iot-hub/iot-hub-item.models.ts | 17 +++++++++++++ 10 files changed, 54 insertions(+), 82 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html index 7bbff19a85..545d219596 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html @@ -29,7 +29,7 @@ {{ 'iot-hub.item-type' | translate }} - {{ getItemTypeIcon(item.itemType) }} + {{ getItemTypeIcon(item.itemType) }} {{ getItemTypeLabel(item.itemType) }} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss index d83bb3e192..1edef65112 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss @@ -122,12 +122,14 @@ --mat-icon-color: currentColor; } -.tb-type-widget { color: rgb(32, 115, 61); background: rgba(46, 166, 88, 0.06); } -.tb-type-dashboard { color: rgb(61, 76, 166); background: rgba(61, 76, 166, 0.06); } -.tb-type-solution-template { color: rgb(0, 137, 186); background: rgba(0, 137, 186, 0.06); } -.tb-type-calc-field { color: rgb(40, 120, 148); background: rgba(40, 120, 148, 0.06); } -.tb-type-rule-chain { color: rgb(178, 121, 29); background: rgba(179, 121, 29, 0.06); } -.tb-type-device { color: rgb(24, 146, 110); background: rgba(24, 146, 110, 0.06); } +// Colors mirror itemTypeChipColors from the iot-hub mp-item.models. +.tb-type-device { color: #3d4ca6; background: rgba(61, 76, 166, 0.06); } +.tb-type-solution-template { color: #2c6cb4; background: rgba(44, 108, 180, 0.06); } +.tb-type-widget { color: #20733d; background: rgba(46, 166, 88, 0.06); } +.tb-type-calc-field { color: #3db5e0; background: rgba(61, 181, 224, 0.06); } +.tb-type-alarm-rule { color: #d7702f; background: rgba(215, 112, 47, 0.06); } +.tb-type-rule-chain { color: #aa5be3; background: rgba(170, 91, 227, 0.06); } +.tb-type-dashboard { color: #607d8b; background: rgba(96, 125, 139, 0.06); } // "vX.X.X Available" button — Design: border rgba(0,0,0,0.12), rounded-4, px-12 py-6, gap-8 .tb-installed-version-btn { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts index 09917528be..b625d96e2b 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts @@ -44,7 +44,7 @@ import { ItemPublishedVersionInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; -import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; +import { getItemTypeIcon, ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; import { EntityType } from '@shared/models/entity-type.models'; import { getEntityDetailsPageURL } from '@core/utils'; import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; @@ -153,16 +153,7 @@ export class TbIotHubInstalledItemsTableComponent implements OnInit, OnChanges, } getItemTypeIcon(itemType: string): string { - switch (itemType) { - case 'WIDGET': return 'widgets'; - case 'DASHBOARD': return 'dashboard'; - case 'SOLUTION_TEMPLATE': return 'integration_instructions'; - case 'CALCULATED_FIELD': return 'functions'; - case 'ALARM_RULE': return 'notification_important'; - case 'RULE_CHAIN': return 'settings_ethernet'; - case 'DEVICE': return 'memory'; - default: return 'category'; - } + return getItemTypeIcon(itemType); } deleteItem(item: IotHubInstalledItem): void { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts index 38c1e26dcf..bdf50acddf 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.ts @@ -16,7 +16,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTranslations, widgetTypeTranslations } from '@shared/models/iot-hub/iot-hub-version.models'; -import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; +import { getItemTypeIcon, ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { TranslateService } from '@ngx-translate/core'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; @@ -69,17 +69,21 @@ export class TbIotHubItemCardComponent { getPlaceholderIcon(): string { switch (this.item.type) { - case ItemType.WIDGET: return 'widgets'; - case ItemType.DASHBOARD: return 'dashboard'; - case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions'; case ItemType.CALCULATED_FIELD: - return this.item.icon || cfTypeIcons.get(this.item.dataDescriptor?.cfType) || 'functions'; + return this.item.icon + || cfTypeIcons.get(this.item.dataDescriptor?.cfType) + || getItemTypeIcon(ItemType.CALCULATED_FIELD); case ItemType.ALARM_RULE: - return this.item.icon || 'notification_important'; + return this.item.icon || getItemTypeIcon(ItemType.ALARM_RULE); case ItemType.RULE_CHAIN: - return this.item.icon || (this.item.dataDescriptor?.ruleChainType === 'EDGE' ? 'router' : 'device_hub'); - case ItemType.DEVICE: return 'memory'; - default: return 'extension'; + return this.item.icon + || (this.item.dataDescriptor?.ruleChainType === 'EDGE' + ? 'router' + : (this.item.dataDescriptor?.ruleChainType === 'CORE' + ? 'device_hub' + : getItemTypeIcon(ItemType.RULE_CHAIN))); + default: + return getItemTypeIcon(this.item.type); } } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts index c770045ddc..382f2d0f88 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts @@ -21,7 +21,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { DialogComponent } from '@shared/components/dialog.component'; import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTranslations, widgetTypeTranslations, NodeInfo } from '@shared/models/iot-hub/iot-hub-version.models'; -import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; +import { getItemTypeIcon, ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { TranslateService } from '@ngx-translate/core'; @@ -103,16 +103,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent } @else { - {{ getTypeIcon() }} + {{ getTypeIcon() }} } } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.ts index c2ca0ba1ea..743f02bea4 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.ts @@ -17,7 +17,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; -import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; +import { getItemTypeIcon, ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; type CardState = 'loading' | 'loaded' | 'unavailable'; @@ -71,29 +71,11 @@ export class TbIotHubItemLinkCardComponent implements OnInit { getCompactIcon(): string { const item = this.item!; - if (item.icon) { - return item.icon; - } - switch (item.type) { - case ItemType.CALCULATED_FIELD: return 'functions'; - case ItemType.ALARM_RULE: return 'notification_important'; - case ItemType.RULE_CHAIN: return 'account_tree'; - default: return 'category'; - } + return item.icon || getItemTypeIcon(item.type); } getTypeIcon(): string { - const item = this.item!; - switch (item.type) { - case ItemType.WIDGET: return 'widgets'; - case ItemType.DASHBOARD: return 'dashboard'; - case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions'; - case ItemType.CALCULATED_FIELD: return 'functions'; - case ItemType.ALARM_RULE: return 'notification_important'; - case ItemType.RULE_CHAIN: return 'account_tree'; - case ItemType.DEVICE: return 'memory'; - default: return 'category'; - } + return getItemTypeIcon(this.item!.type); } getCompactColor(): string { diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html index 682411d884..f9f7285c3b 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html @@ -109,7 +109,7 @@ @if (getItemImage(item); as imgUrl) { } @else { - {{ getItemTypeIcon(item.type) }} + {{ getItemTypeIcon(item.type) }} } } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts index 72a5513056..c8fff1ea7e 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts @@ -25,7 +25,7 @@ import { MediaBreakpoints } from '@shared/models/constants'; import { PageLink } from '@shared/models/page/page-link'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; -import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; +import { getItemTypeIcon, ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { TranslateService } from '@ngx-translate/core'; @@ -266,15 +266,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { } getCompactIcon(item: MpItemVersionView): string { - if (item.icon) { - return item.icon; - } - switch (item.type) { - case ItemType.CALCULATED_FIELD: return 'functions'; - case ItemType.ALARM_RULE: return 'notification_important'; - case ItemType.RULE_CHAIN: return 'settings_ethernet'; - default: return 'category'; - } + return item.icon || getItemTypeIcon(item.type); } getItemImage(item: MpItemVersionView): string | null { @@ -282,14 +274,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy { } getItemTypeIcon(type: ItemType): string { - switch (type) { - case ItemType.WIDGET: return 'widgets'; - case ItemType.DASHBOARD: return 'dashboard'; - case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions'; - case ItemType.ALARM_RULE: return 'notification_important'; - case ItemType.DEVICE: return 'memory'; - default: return 'category'; - } + return getItemTypeIcon(type); } getSearchGroupLabel(type: ItemType): string { diff --git a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts index 6451bc5d52..6c528384c4 100644 --- a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts +++ b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts @@ -36,6 +36,23 @@ export const itemTypeTranslations = new Map( ] ); +// Canonical icon lookup per item type. Values are tb-icon +// identifiers (Material symbol names or `mdi:*` strings) and should +// be used everywhere an icon is rendered for an item type so the +// mapping stays consistent across the app. +export const itemTypeIcons: Record = { + [ItemType.WIDGET]: 'widgets', + [ItemType.DASHBOARD]: 'dashboard', + [ItemType.SOLUTION_TEMPLATE]: 'apps', + [ItemType.CALCULATED_FIELD]: 'mdi:function-variant', + [ItemType.RULE_CHAIN]: 'settings_ethernet', + [ItemType.ALARM_RULE]: 'mdi:bell-cog', + [ItemType.DEVICE]: 'devices_other' +}; + +export const getItemTypeIcon = (type?: string | null): string => + type && itemTypeIcons[type] ? itemTypeIcons[type] : 'category'; + /** * Item types discoverable to creators in the marketplace UI. * DASHBOARD is intentionally absent (IoT Hub no longer accepts Dashboard contributions). From 18232e9c8c7acc3ec0e84be93b7b4898b407d295 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 10 Jun 2026 19:49:13 +0300 Subject: [PATCH 2/3] feat(iot-hub): force markdown links to new tab + verified item-link card - TbIotHubMarkdownComponent.parseData now runs through a forceLinksOpenInNewTab post-pass that (a) appends the {:target="_blank"} suffix to the TEXT of any markdown link [text](url) that doesn't already carry it, matching the MarkedOptionsService.renderer.link contract that strips the suffix and rewrites the with target="_blank", and (b) injects target="_blank" into raw HTML anchors that don't already declare a target= attribute. Image syntax ![alt](url) is excluded via lookbehind. - iot-hub-markdown SCSS narrows the link styling selector to a:not(.mdc-button):not(.tb-iot-hub-item-link-card) so the embedded item-link card anchor keeps its own typography. - iot-hub-item-link-card now renders a verified icon (teal #00695c via a new tb-iot-hub-item-link-verified-icon class) when item.creatorVerified is true, matching the item-card treatment; the default person icon is used otherwise. --- .../iot-hub-item-link-card.component.html | 6 ++++- .../iot-hub-item-link-card.component.scss | 8 ++++++ .../iot-hub/iot-hub-markdown.component.scss | 2 +- .../iot-hub/iot-hub-markdown.component.ts | 27 +++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.html index 8d2d8b25c9..fbf00c7dd7 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.html @@ -47,7 +47,11 @@ diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.scss index 2713303d6c..cdf4005199 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-link-card/iot-hub-item-link-card.component.scss @@ -37,6 +37,9 @@ background: rgba(4, 138, 211, 0.04); border-color: rgba(4, 138, 211, 0.32); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); + .tb-iot-hub-item-link-name, .tb-iot-hub-item-link-author { + text-decoration: underline; + } } &--unavailable { @@ -128,6 +131,11 @@ width: 14px; height: 14px; } + + // Verified-creator accent — matches the item card chip color. + .tb-iot-hub-item-link-verified-icon { + color: #00695c; + } } .tb-iot-hub-item-link-skeleton-block, diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss index 57e3d25101..be2ab69a76 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss @@ -128,7 +128,7 @@ $gallery-border-hover-color: #2a7dec; max-width: 100%; } - a:not(.mdc-button) { + a:not(.mdc-button):not(.tb-iot-hub-item-link-card) { font-weight: 500; color: $link-color; text-decoration: none; diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts index 57df5a8074..a63d17c914 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts @@ -109,9 +109,36 @@ export class TbIotHubMarkdownComponent implements OnInit, OnChanges { parsed = replaceItemLinkPlaceholders(parsed); parsed = this.resolveImages(parsed); parsed = this.resolveVariables(parsed); + parsed = this.forceLinksOpenInNewTab(parsed); return parsed; } + // Ensure every link rendered through tb-markdown opens in a new tab. + // 1) For markdown links `[text](url)` — append the + // `{:target="_blank"}` suffix to the link TEXT (not the URL) + // so MarkedOptionsService.renderer.link, which checks + // `token.text.endsWith(targetBlankBlock)`, emits + // `target="_blank"` on the rendered . Skip links whose + // text already ends with the suffix and skip image syntax + // (`![alt](url)`). + // 2) For raw HTML anchors authored in the markdown — add + // `target="_blank"` when no `target=` attribute is already + // present. + private forceLinksOpenInNewTab(content: string): string { + const TARGET_BLANK_BLOCK = '{:target="_blank"}'; + content = content.replace( + /(? + text.endsWith(TARGET_BLANK_BLOCK) ? match : `[${text}${TARGET_BLANK_BLOCK}](${url})` + ); + content = content.replace( + /]*)>/gi, + (match, attrs: string) => + /\btarget\s*=/i.test(attrs) ? match : `` + ); + return content; + } + private prefixResourceUrls(markdown: string): string { const baseUrl = this.iotHubApiService.baseUrl; return markdown.replace(/([("])(\/api\/resources\/[^)"]*)/g, `$1${baseUrl}$2`); From 8afc94cfc97728cc8b58723511de90980a40d036 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 10 Jun 2026 20:23:45 +0300 Subject: [PATCH 3/3] feat(chip-overflow): optional showOverflowedTitle native tooltip - Add a `showOverflowedTitle` boolean input (defaults to false, coerceBoolean-decorated) to ChipOverflowDirective. When true and hiddenCount > 0, the overflow `+N` chip gets a `title` attribute set to the comma-joined trimmed textContent of every overflowing chip, surfacing them via the native browser tooltip. When the flag is off, no items overflow, or hiddenCount drops back to zero, the title attribute is removed. - Keep `pointer-events` in sync with the title so the native tooltip actually fires: remove `pointer-events: none` while a title is present; reapply it otherwise (matches the initial state set in createOverflowChip). - Opt the IoT Hub item detail dialog's three tb-chip-overflow meta-chip groups (connectivity, categories, use-cases) into showOverflowedTitle so hovering the `+N` chip lists the hidden values. --- .../iot-hub-item-detail-dialog.component.html | 6 ++--- .../directives/chip-overflow.directive.ts | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html index 6c0318abbc..3827599242 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html @@ -203,7 +203,7 @@ @if (item.dataDescriptor?.connectivity?.length) {
{{ 'iot-hub.connectivity' | translate }} -
+
@for (conn of item.dataDescriptor.connectivity; track conn) { {{ conn }} } @@ -214,7 +214,7 @@ @if (item.categories?.length) {
{{ 'iot-hub.category' | translate }} -
+
@for (cat of item.categories; track cat) { {{ cat }} } @@ -225,7 +225,7 @@ @if (item.useCases?.length) {
{{ 'iot-hub.use-cases' | translate }} -
+
@for (uc of item.useCases; track uc) { {{ uc }} } diff --git a/ui-ngx/src/app/shared/directives/chip-overflow.directive.ts b/ui-ngx/src/app/shared/directives/chip-overflow.directive.ts index 7ed84f10e7..10585f1453 100644 --- a/ui-ngx/src/app/shared/directives/chip-overflow.directive.ts +++ b/ui-ngx/src/app/shared/directives/chip-overflow.directive.ts @@ -15,6 +15,7 @@ /// import { AfterViewInit, Directive, ElementRef, Input, NgZone, OnDestroy, Renderer2 } from '@angular/core'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Directive({ selector: '[tb-chip-overflow]', @@ -28,6 +29,10 @@ export class ChipOverflowDirective implements AfterViewInit, OnDestroy { @Input() overflowClass = 'tb-overflow-chip'; @Input() minChips = 1; + @Input() + @coerceBoolean() + showOverflowedTitle = false; + private resizeObserver?: ResizeObserver; private mutationObserver?: MutationObserver; private overflowEl!: HTMLElement; @@ -151,6 +156,7 @@ export class ChipOverflowDirective implements AfterViewInit, OnDestroy { // 4. Determine which chips fit let usedWidth = 0; let hiddenCount = 0; + const hiddenChips: HTMLElement[] = []; const minChips = Math.max(1, this.minChips); for (let i = 0; i < chips.length; i++) { @@ -163,12 +169,29 @@ export class ChipOverflowDirective implements AfterViewInit, OnDestroy { usedWidth = nextUsedWidth; } else { hiddenCount++; + hiddenChips.push(chips[i]); } } if (hiddenCount > 0) { this.renderer.setProperty(this.overflowEl, 'textContent', this.overflowTemplate.replace('{n}', String(hiddenCount))); this.renderer.setStyle(this.overflowEl, 'display', 'inline-flex'); + if (this.showOverflowedTitle) { + const title = hiddenChips + .map(chip => (chip.textContent || '').trim()) + .filter(text => !!text) + .join(', '); + this.renderer.setAttribute(this.overflowEl, 'title', title); + // Allow pointer interaction so the browser's native title + // tooltip appears on hover. + this.renderer.removeStyle(this.overflowEl, 'pointerEvents'); + } else { + this.renderer.removeAttribute(this.overflowEl, 'title'); + this.renderer.setStyle(this.overflowEl, 'pointerEvents', 'none'); + } + } else { + this.renderer.removeAttribute(this.overflowEl, 'title'); + this.renderer.setStyle(this.overflowEl, 'pointerEvents', 'none'); } } }