}
@@ -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/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 1579bc763c..6f8c6e5552 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() }}
}
}
{{ item.name }}
- person
+ @if (item.creatorVerified) {
+ verified
+ } @else {
+ person
+ }
{{ item.creatorDisplayName }}
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-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/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
+ // (``).
+ // 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`);
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 {
-
+
}
}
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/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');
}
}
}
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).