From 6360f57d2582668732ad923ee0aa4b81c3e6924a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 10 Apr 2026 19:32:50 +0300 Subject: [PATCH] Extract reusable search component, redesign creator profile, add showCreator Search component: - New TbIotHubSearchComponent in home/components/iot-hub/ with search toolbar, pagination, grouped results, sort, loading/empty states - Inputs: searchText (two-way), creatorId, showCreator; ng-content for title - Custom search input (320x40px, border, search icon, clear button) - Sort icon-only button on lt-sm, mobile results count in title row - Divider before categories section Search page: - Renamed to TbIotHubSearchPageComponent, embeds TbIotHubSearchComponent - Title projected via ng-content, responsive inset padding per breakpoint - Full-height layout with flex Creator profile: - Two-column layout: creator info (360px) + vertical divider + published items - Uses TbIotHubSearchComponent with creatorId and showCreator=false - 140px avatar, 28px name, description, link rows with hover styles - Responsive: column layout on lt-lg, adjustable padding per breakpoint - Back button inside creator info column Item detail dialog: - showCreator parameter (default true), hides author section when false - openItemDetail service method accepts showCreator argument Item card: - Respects showCreator input, hides author in both compact and big layouts --- .../iot-hub/iot-hub-actions.service.ts | 5 +- .../iot-hub/iot-hub-components.module.ts | 7 +- .../iot-hub/iot-hub-item-card.component.html | 16 +- .../iot-hub-item-detail-dialog.component.html | 20 +- .../iot-hub-item-detail-dialog.component.ts | 3 + .../iot-hub/iot-hub-search.component.html | 143 ++++++++ .../iot-hub/iot-hub-search.component.scss | 140 +++++--- .../iot-hub/iot-hub-search.component.ts | 44 +-- .../iot-hub-creator-profile.component.html | 115 ++++--- .../iot-hub-creator-profile.component.scss | 312 ++++++++++++++---- .../pages/iot-hub/iot-hub-routing.module.ts | 4 +- .../iot-hub-search-page.component.html | 36 ++ .../iot-hub-search-page.component.scss | 155 +++++++++ .../iot-hub/iot-hub-search-page.component.ts | 45 +++ .../iot-hub/iot-hub-search.component.html | 148 --------- .../home/pages/iot-hub/iot-hub.module.ts | 4 +- 16 files changed, 838 insertions(+), 359 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.html rename ui-ngx/src/app/modules/home/{pages => components}/iot-hub/iot-hub-search.component.scss (75%) rename ui-ngx/src/app/modules/home/{pages => components}/iot-hub/iot-hub-search.component.ts (90%) create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search-page.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search-page.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search-page.component.ts delete mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.html diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts index 07d4f9cfc0..79b9d42562 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts @@ -38,11 +38,12 @@ export class IotHubActionsService { private iotHubApiService: IotHubApiService ) {} - openItemDetail(item: MpItemVersionView, installedItem?: IotHubInstalledItem, mode?: IotHubItemDetailDialogMode): Observable { + openItemDetail(item: MpItemVersionView, installedItem?: IotHubInstalledItem, + mode?: IotHubItemDetailDialogMode, showCreator?: boolean): Observable { return this.dialog.open(TbIotHubItemDetailDialogComponent, { panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], autoFocus: false, - data: { item, installedItem, mode } as IotHubItemDetailDialogData + data: { item, installedItem, mode, showCreator } as IotHubItemDetailDialogData }).afterClosed(); } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts index ec02acc2ff..5d636b829e 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts @@ -25,6 +25,7 @@ import { TbIotHubInstallDialogComponent } from './iot-hub-install-dialog.compone import { TbIotHubUpdateDialogComponent } from './iot-hub-update-dialog.component'; import { TbIotHubDeleteDialogComponent } from './iot-hub-delete-dialog.component'; import { TbDeviceInstallDialogComponent } from './device-install-dialog/device-install-dialog.component'; +import { TbIotHubSearchComponent } from './iot-hub-search.component'; import { IotHubActionsService } from './iot-hub-actions.service'; @NgModule({ @@ -36,7 +37,8 @@ import { IotHubActionsService } from './iot-hub-actions.service'; TbIotHubInstallDialogComponent, TbIotHubUpdateDialogComponent, TbIotHubDeleteDialogComponent, - TbDeviceInstallDialogComponent + TbDeviceInstallDialogComponent, + TbIotHubSearchComponent ], imports: [ CommonModule, @@ -53,7 +55,8 @@ import { IotHubActionsService } from './iot-hub-actions.service'; TbIotHubInstallDialogComponent, TbIotHubUpdateDialogComponent, TbIotHubDeleteDialogComponent, - TbDeviceInstallDialogComponent + TbDeviceInstallDialogComponent, + TbIotHubSearchComponent ] }) export class IotHubComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html index cca6402cde..66432ab1a1 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html @@ -27,9 +27,11 @@ }

{{ item.name }}

- person - {{ item.creatorDisplayName }} - · + @if (showCreator) { + person + {{ item.creatorDisplayName }} + · + } download {{ item.totalInstallCount | shortNumber }}
@@ -74,9 +76,11 @@ }

{{ item.name }}

} -
- person - {{ item.creatorDisplayName }} -
+ @if (showCreator) { +
+ person + {{ item.creatorDisplayName }} +
+ }
@if (mode === 'add') { @@ -198,10 +200,12 @@
} -
- person - {{ item.creatorDisplayName }} -
+ @if (showCreator) { +
+ person + {{ item.creatorDisplayName }} +
+ }
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 0d50a1b596..afaf5cb39a 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 @@ -40,6 +40,7 @@ export interface IotHubItemDetailDialogData { item: MpItemVersionView; installedItem?: IotHubInstalledItem; mode?: IotHubItemDetailDialogMode; + showCreator?: boolean; } @Component({ @@ -53,6 +54,7 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent +
+ + @if (!isLoading && totalElements > 0) { + + {{ 'iot-hub.results-count' | translate:{ count: totalElements } }} + + } +
+ +
+
+ search + + @if (searchText) { + + } +
+ + + + @for (option of sortOptions; track option.value; let i = $index) { + + } + +
+ + +@if (!isLoading && totalElements > 0) { +
+ + {{ 'iot-hub.results-count' | translate:{ count: totalElements } }} + +
+
+ + @for (p of getPageNumbers(); track p) { + + } + +
+
+ {{ 'iot-hub.items-per-page' | translate }}: + + @for (size of pageSizeOptions; track size) { + {{ size }} + } + +
+
+
+} + + +@if (!isLoading && totalElements === 0 && searchText?.trim()) { +
+ + {{ 'iot-hub.results-count' | translate:{ count: 0 } }} + +
+} + + +@if (isLoading) { +
+ +
+} + + +@if (!isLoading && totalElements === 0 && searchText?.trim()) { +
+ search_off +

{{ 'iot-hub.no-items-found' | translate }}

+

{{ 'iot-hub.try-adjusting-search' | translate }}

+
+} + + +@if (!isLoading && resultGroups.length > 0) { +
+
+ @for (group of resultGroups; track group.type) { +
+ + + +
+ @for (item of group.items; track item.id) { + + + } +
+
+ } +
+} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.scss similarity index 75% rename from ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.scss rename to ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.scss index 8e52b10424..003c908b45 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.scss @@ -17,36 +17,21 @@ @import "../../../../../scss/constants"; :host { - display: block; - padding: 16px; -} - -.tb-search-page { - background: white; - border-radius: 8px; - padding: 40px; display: flex; flex-direction: column; gap: 40px; } -// Back button -.tb-search-back-btn { - align-self: flex-start; -} - -// Title — Design: 28px Medium, black-primary -.tb-search-title { - font-size: 28px; - font-weight: 500; - line-height: 36px; - letter-spacing: 0.25px; - color: rgba(0, 0, 0, 0.87); - margin: 0; +// Title row +.tb-search-title-row { + display: flex; + align-items: baseline; + gap: 16px; } -.tb-search-title-query { - color: rgba(0, 0, 0, 0.54); +.tb-search-results-count-mobile { + display: none; + margin-left: auto; } // Search + Sort toolbar @@ -57,10 +42,55 @@ } .tb-search-input-field { - width: 480px; + width: 320px; + min-width: 0; + height: 40px; + display: flex; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 8px; + background: white; +} + +.tb-search-input-icon { + font-size: 24px; + width: 24px; + height: 24px; + color: rgba(0, 0, 0, 0.38); + flex-shrink: 0; +} + +.tb-search-input { + flex: 1; + min-width: 0; + border: none; + outline: none; + font-size: 14px; + font-weight: 400; + line-height: 20px; + color: rgba(0, 0, 0, 0.87); + background: transparent; + font-family: inherit; - ::ng-deep .mat-mdc-text-field-wrapper { - border-radius: 8px; + &::placeholder { + color: rgba(0, 0, 0, 0.38); + } +} + +.tb-search-input-clear.mat-mdc-icon-button { + width: 24px; + height: 24px; + padding: 0; + flex-shrink: 0; + color: rgba(0, 0, 0, 0.38); + --mdc-icon-button-state-layer-size: 24px; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; } } @@ -72,6 +102,10 @@ margin-left: auto; } +.tb-search-sort-btn-icon { + display: none; +} + // Results count + Pagination row .tb-search-pagination-row { display: flex; @@ -85,6 +119,7 @@ font-weight: 500; line-height: 20px; color: rgba(0, 0, 0, 0.76); + white-space: nowrap; } .tb-search-pagination { @@ -154,7 +189,7 @@ margin-top: -20px; } -// Empty state — Design: centered, 100px icon, 20px title, 14px subtitle +// Empty state .tb-search-empty { display: flex; flex-direction: column; @@ -189,15 +224,25 @@ margin: 0; } +// Divider before categories +.tb-search-divider { + background: rgba(0, 0, 0, 0.12); + height: 1px; + margin-bottom: -20px; + margin-top: -20px; +} + // Categories / sections .tb-search-categories { display: flex; flex-direction: column; gap: 48px; margin-top: -20px; + overflow: auto; + padding-top: 20px; } -// Section header — Design: 18px Medium, clickable, right chevron +// Section header .tb-search-section-header { font-size: 18px; font-weight: 500; @@ -213,7 +258,7 @@ } } -// Card grid — base: ≥1920 5-col big, 3-col compact +// Card grid .tb-search-card-grid { display: grid; grid-template-columns: repeat(5, 1fr); @@ -224,7 +269,7 @@ } } -// Responsive — same columns as iot-hub home +// Responsive @media #{$mat-gt-xxl} { .tb-search-card-grid { @@ -257,14 +302,6 @@ } @media #{$mat-lt-lg} { - .tb-search-page { - padding: 24px; - } - - .tb-search-input-field { - width: 400px; - } - .tb-search-pagination-row { flex-direction: column; align-items: flex-start; @@ -290,24 +327,31 @@ } @media #{$mat-lt-md} { - .tb-search-input-field { - width: 100%; - flex: 1; + .tb-search-title-row { + margin-bottom: -24px; + } + + .tb-search-results-count-mobile { + display: inline; + } + + .tb-search-pagination-row .tb-search-results-count { + display: none; } } @media #{$mat-lt-sm} { - .tb-search-page { - padding: 16px; + .tb-search-input-field { + width: 100%; + flex: 1; } - .tb-search-toolbar { - flex-wrap: wrap; + .tb-search-sort-btn-full { + display: none; } - .tb-search-sort-btn { - width: 100%; - justify-content: flex-start; + .tb-search-sort-btn-icon { + display: inline-flex; } .tb-search-card-grid { diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts similarity index 90% rename from ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.ts rename to ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts index f4cc250b51..b93da0fa20 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts @@ -14,8 +14,8 @@ /// limitations under the License. /// -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; +import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { forkJoin, Subject, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; @@ -25,7 +25,7 @@ import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/io import { 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 { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; +import { IotHubActionsService } from './iot-hub-actions.service'; interface SearchResultGroup { type: ItemType; @@ -53,7 +53,11 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { readonly ItemType = ItemType; - searchText = ''; + @Input() searchText = ''; + @Input() creatorId: string; + @Input() showCreator = true; + @Output() searchTextChange = new EventEmitter(); + resultGroups: SearchResultGroup[] = []; totalElements = 0; isLoading = false; @@ -76,7 +80,6 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { private searchSubscription: Subscription; constructor( - private route: ActivatedRoute, private router: Router, private translate: TranslateService, private iotHubApiService: IotHubApiService, @@ -96,11 +99,6 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { ).subscribe(result => { this.applyResults(result.data, result.totalElements); }); - - const search = this.route.snapshot.queryParamMap.get('search'); - if (search) { - this.searchText = search; - } this.loadResults(); } @@ -108,16 +106,14 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { this.searchSubscription?.unsubscribe(); } - navigateBack(): void { - this.router.navigate(['/iot-hub']); - } - onSearchInput(): void { + this.searchTextChange.emit(this.searchText); this.searchSubject.next(this.searchText || ''); } clearSearch(): void { this.searchText = ''; + this.searchTextChange.emit(this.searchText); this.loadResults(); } @@ -142,13 +138,17 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { return Array.from({length: total}, (_, i) => i); } const pages: number[] = []; - let start = Math.max(0, this.pageIndex - 2); - let end = Math.min(total - 1, start + 4); + const start = Math.max(0, this.pageIndex - 2); + const end = Math.min(total - 1, start + 4); if (end - start < 4) { - start = Math.max(0, end - 4); - } - for (let i = start; i <= end; i++) { - pages.push(i); + const adjustedStart = Math.max(0, end - 4); + for (let i = adjustedStart; i <= end; i++) { + pages.push(i); + } + } else { + for (let i = start; i <= end; i++) { + pages.push(i); + } } return pages; } @@ -207,7 +207,7 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { // Dialogs openItemDetail(item: MpItemVersionView): void { - this.iotHubActions.openItemDetail(item, this.getInstalledItem(item)).subscribe(result => { + this.iotHubActions.openItemDetail(item, this.getInstalledItem(item), undefined, this.showCreator).subscribe(result => { if (result === 'installed' || result === 'deleted' || result === 'updated') { this.reloadInstalledItems(); } @@ -256,7 +256,7 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy { const sort = this.sortOptions[this.selectedSortIndex]; const sortOrder: SortOrder = { property: sort.value, direction: sort.direction }; const pageLink = new PageLink(this.pageSize, this.pageIndex, text.trim() || null, sortOrder); - const query = new MpItemVersionQuery(pageLink); + const query = new MpItemVersionQuery(pageLink, undefined, undefined, this.creatorId || undefined); return this.iotHubApiService.getPublishedVersions(query, { ignoreLoading: true }); } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.html index d664a3bbfd..88a3e1c22c 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.html +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.html @@ -15,71 +15,70 @@ limitations under the License. --> -
- - - +
@if (creator) { - -
-
-
- @if (getAvatarUrl(); as avatarUrl) { - - } @else { - person + +
+ + +
+
+ @if (getAvatarUrl(); as avatarUrl) { + + } @else { + person + } +
+

{{ creator.displayName }}

+ @if (creator.description) { +

{{ creator.description }}

+ } + -
-

{{ creator.displayName }}

@if (creator.contactEmail) { - {{ creator.contactEmail }} + + mdi:email-outline + {{ creator.contactEmail }} + + } + @if (creator.githubUrl) { + + mdi:github + GitHub + + } + @if (creator.linkedinUrl) { + + mdi:linkedin + LinkedIn + } + @if (creator.twitterUrl) { + + mdi:twitter + X + + } +
- @if (creator.description) { -

{{ creator.description }}

- } - - -
- - -

- {{ 'iot-hub.published-items' | translate }} ({{ creator.publishedCount }}) -

+ +
- - - + + +

+ {{ 'iot-hub.published-items' | translate }} ({{ creator.publishedCount }}) +

+
}
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.scss index db526c263b..8c4edca2c8 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.scss +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.scss @@ -19,37 +19,44 @@ :host { display: block; padding: 16px; + height: 100%; } -.tb-iot-hub-creator-page { +.tb-creator-page { background: white; border-radius: 8px; - padding: 40px 40px 48px; + padding-left: 123px; overflow: hidden; + display: flex; + height: 100%; } // Back button -.tb-iot-hub-back-btn { - margin-bottom: 24px; +.tb-creator-back-btn { + align-self: flex-start; } -// Creator profile section -.tb-iot-hub-creator-profile { - margin-bottom: 32px; +// Left column: creator info +.tb-creator-info { + width: 360px; + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 48px; + padding: 40px 24px 0 0; } -// Header: avatar + name + email -.tb-iot-hub-creator-header { +.tb-creator-details { display: flex; - align-items: center; + flex-direction: column; gap: 16px; - margin-bottom: 16px; } -.tb-iot-hub-creator-avatar { - width: 64px; - height: 64px; - min-width: 64px; +// Avatar +.tb-creator-avatar { + width: 140px; + height: 140px; + min-width: 140px; border-radius: 50%; overflow: hidden; display: flex; @@ -64,81 +71,264 @@ } mat-icon { - font-size: 40px; - width: 40px; - height: 40px; + font-size: 48px; + width: 48px; + height: 48px; color: rgba(0, 0, 0, 0.3); } } -.tb-iot-hub-creator-name-block { - display: flex; - flex-direction: column; - gap: 2px; -} - -.tb-iot-hub-creator-name { - font-size: 24px; +// Name +.tb-creator-name { + font-size: 28px; font-weight: 500; - line-height: 32px; + line-height: 36px; + letter-spacing: 0.07px; color: rgba(0, 0, 0, 0.87); margin: 0; } -.tb-iot-hub-creator-email { - font-size: 14px; - color: rgba(0, 0, 0, 0.54); -} - // Description -.tb-iot-hub-creator-desc { - font-size: 14px; - line-height: 20px; +.tb-creator-desc { + font-size: 16px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.15px; color: rgba(0, 0, 0, 0.76); - margin: 0 0 16px; - max-width: 600px; + margin: 0; + max-width: 800px; } -// Links: website button + social icons -.tb-iot-hub-creator-links { +// Links +.tb-creator-links { + display: flex; + flex-direction: column; + gap: 4px; +} + +.tb-creator-link { display: flex; align-items: center; gap: 8px; + padding: 4px 0; + height: 32px; + font-size: 14px; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.54); + text-decoration: none; + border-bottom: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &:hover { + border-bottom: none; + } - .mat-mdc-icon-button { - width: 40px; - height: 40px; - padding: 8px; + mat-icon, tb-icon { + font-size: 24px; + width: 24px; + height: 24px; + flex-shrink: 0; color: rgba(0, 0, 0, 0.54); - --mdc-icon-button-state-layer-size: 40px; - --mdc-icon-button-icon-color: rgba(0, 0, 0, 0.54); + } + + &.tb-creator-link-primary { + color: $tb-primary-color; + + mat-icon { + color: $tb-primary-color; + } } } -.tb-iot-hub-creator-website { +a.tb-creator-link:hover span { color: $tb-primary-color; - font-size: 14px; - font-weight: 500; - text-decoration: none; - height: 40px; + text-decoration: underline; +} - &.mat-primary { - --mat-button-outlined-outline-color: #{$tb-primary-color}; - } +// Vertical divider (desktop) +.tb-creator-divider { + width: 1px; + background: rgba(0, 0, 0, 0.12); + flex-shrink: 0; + align-self: stretch; } -// Browse component — stretch to full width and bottom, ignore parent padding -tb-iot-hub-browse { - margin-left: -40px; - margin-right: -40px; - margin-bottom: 0; +// Right column: published items +.tb-creator-items { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + padding-top: 40px; + padding-left: 24px; + padding-right: 123px; + + ::ng-deep { + .tb-search-divider { + margin-left: -24px; + margin-right: -123px; + } + + .tb-search-categories { + padding-right: 123px; + padding-bottom: 40px; + margin-right: -123px; + } + + .tb-search-card-grid { + grid-template-columns: repeat(5, 1fr); + + &.tb-search-card-grid-compact { + grid-template-columns: repeat(3, 1fr); + } + } + } } -// Published items heading -.tb-iot-hub-published-heading { +.tb-creator-items-heading { font-size: 20px; font-weight: 600; line-height: 24px; + letter-spacing: 0.1px; color: rgba(0, 0, 0, 0.87); - margin: 0 0 16px; + margin: 0 0 24px; +} + +// Responsive + +@media #{$mat-lt-xxl} { + .tb-creator-page { + padding-left: 40px; + } + + .tb-creator-items { + padding-right: 40px; + + ::ng-deep { + .tb-search-divider { + margin-right: -40px; + } + + .tb-search-categories { + padding-right: 40px; + margin-right: -40px; + } + + .tb-search-card-grid { + grid-template-columns: repeat(4, 1fr); + } + } + } +} + +@media #{$mat-lt-xl} { + .tb-creator-info { + width: 300px; + } + + .tb-creator-items { + ::ng-deep { + .tb-search-card-grid { + grid-template-columns: repeat(3, 1fr); + + &.tb-search-card-grid-compact { + grid-template-columns: repeat(2, 1fr); + } + } + } + } +} + +@media #{$mat-lt-xmd} { + + .tb-creator-items { + ::ng-deep { + .tb-search-card-grid { + grid-template-columns: repeat(2, 1fr); + + &.tb-search-card-grid-compact { + grid-template-columns: repeat(1, 1fr); + } + } + } + } +} + +@media #{$mat-lt-lg} { + :host { + height: auto; + } + + .tb-creator-page { + flex-direction: column; + padding: 0; + gap: 40px; + } + + .tb-creator-info { + width: 100%; + padding: 40px 24px 0 24px; + } + + .tb-creator-items { + padding-top: 0; + padding-right: 24px; + + ::ng-deep { + .tb-search-divider { + margin-right: -24px; + } + + .tb-search-categories { + padding-right: 24px; + margin-right: -24px; + } + } + } + + .tb-creator-items-heading { + margin-bottom: 0; + } + + .tb-creator-divider { + width: 100%; + height: 1px; + align-self: auto; + } +} + +@media #{$mat-lt-sm} { + .tb-creator-page { + gap: 24px; + } + + .tb-creator-info { + padding: 16px 16px 0 16px; + gap: 24px; + } + + .tb-creator-items { + padding-left: 16px; + padding-right: 16px; + + ::ng-deep { + .tb-search-divider { + margin-left: -16px; + margin-right: -16px; + } + + .tb-search-categories { + padding-right: 16px; + margin-right: -16px; + } + + .tb-search-card-grid { + grid-template-columns: repeat(1, 1fr); + } + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts index d83266032b..b917f51f6e 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts @@ -22,7 +22,7 @@ import { TbIotHubHomeComponent } from './iot-hub-home.component'; import { TbIotHubItemsPageComponent } from './iot-hub-items-page.component'; import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component'; import { TbIotHubInstalledItemsComponent } from './iot-hub-installed-items.component'; -import { TbIotHubSearchComponent } from './iot-hub-search.component'; +import { TbIotHubSearchPageComponent } from './iot-hub-search-page.component'; const routes: Routes = [ { @@ -105,7 +105,7 @@ const routes: Routes = [ }, { path: 'search', - component: TbIotHubSearchComponent, + component: TbIotHubSearchPageComponent, data: { auth: [Authority.TENANT_ADMIN], title: 'iot-hub.search-results', diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search-page.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search-page.component.html new file mode 100644 index 0000000000..7eed22cf70 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search-page.component.html @@ -0,0 +1,36 @@ + +
+ + + + + + +

+ @if (searchText?.trim()) { + {{ 'iot-hub.search-results-for' | translate }}'{{ searchText.trim() }}' + } @else { + {{ 'iot-hub.search-results' | translate }} + } +

+
+
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search-page.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search-page.component.scss new file mode 100644 index 0000000000..8f6ab01297 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search-page.component.scss @@ -0,0 +1,155 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../scss/constants"; + +:host { + display: block; + padding: 16px; + height: 100%; +} + +.tb-search-page { + background: white; + border-radius: 8px; + display: flex; + flex-direction: column; + gap: 40px; + height: 100%; +} + +// Back button +.tb-search-back-btn { + align-self: flex-start; + margin-top: 40px; + margin-left: 123px; +} + +// Title +.tb-search-title { + font-size: 28px; + font-weight: 500; + line-height: 36px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + margin: 0; +} + +.tb-search-title-query { + color: rgba(0, 0, 0, 0.54); +} + +tb-iot-hub-search { + flex: 1; + min-height: 0; + padding-left: 123px; + padding-right: 123px; + + ::ng-deep { + .tb-search-divider { + margin-left: -123px; + margin-right: -123px; + } + + .tb-search-categories { + padding-right: 123px; + padding-bottom: 40px; + margin-right: -123px; + } + } +} + +// Responsive +@media #{$mat-lt-xxl} { + .tb-search-back-btn { + margin-left: 40px; + } + + tb-iot-hub-search { + padding-left: 40px; + padding-right: 40px; + + ::ng-deep { + .tb-search-divider { + margin-left: -40px; + margin-right: -40px; + } + + .tb-search-categories { + padding-right: 40px; + margin-right: -40px; + } + } + } +} + +@media #{$mat-lt-lg} { + .tb-search-page { + gap: 24px; + } + + .tb-search-back-btn { + margin-top: 24px; + margin-left: 24px; + } + + tb-iot-hub-search { + padding-left: 24px; + padding-right: 24px; + + ::ng-deep { + .tb-search-divider { + margin-left: -24px; + margin-right: -24px; + } + + .tb-search-categories { + padding-right: 24px; + padding-bottom: 24px; + margin-right: -24px; + } + } + } +} + +@media #{$mat-lt-sm} { + .tb-search-page { + gap: 16px; + } + + .tb-search-back-btn { + margin-top: 16px; + margin-left: 16px; + } + + tb-iot-hub-search { + padding-left: 16px; + padding-right: 16px; + + ::ng-deep { + .tb-search-divider { + margin-left: -16px; + margin-right: -16px; + } + + .tb-search-categories { + padding-right: 16px; + padding-bottom: 16px; + margin-right: -16px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search-page.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search-page.component.ts new file mode 100644 index 0000000000..dbdc3aa683 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search-page.component.ts @@ -0,0 +1,45 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +@Component({ + selector: 'tb-iot-hub-search-page', + standalone: false, + templateUrl: './iot-hub-search-page.component.html', + styleUrls: ['./iot-hub-search-page.component.scss'] +}) +export class TbIotHubSearchPageComponent implements OnInit { + + searchText = ''; + + constructor( + private route: ActivatedRoute, + private router: Router + ) {} + + ngOnInit(): void { + const search = this.route.snapshot.queryParamMap.get('search'); + if (search) { + this.searchText = search; + } + } + + navigateBack(): void { + this.router.navigate(['/iot-hub']); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.html deleted file mode 100644 index 532417654c..0000000000 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.html +++ /dev/null @@ -1,148 +0,0 @@ - -
- - - - -

- @if (searchText?.trim()) { - {{ 'iot-hub.search-results-for' | translate }}'{{ searchText.trim() }}' - } @else { - {{ 'iot-hub.search-results' | translate }} - } -

- - -
- - - @if (searchText) { - - } - - - - - @for (option of sortOptions; track option.value; let i = $index) { - - } - -
- - - @if (!isLoading && totalElements > 0) { -
- - {{ 'iot-hub.results-count' | translate:{ count: totalElements } }} - -
-
- - @for (p of getPageNumbers(); track p) { - - } - -
-
- {{ 'iot-hub.items-per-page' | translate }}: - - @for (size of pageSizeOptions; track size) { - {{ size }} - } - -
-
-
- } - - - @if (!isLoading && totalElements === 0 && searchText?.trim()) { -
- - {{ 'iot-hub.results-count' | translate:{ count: 0 } }} - -
- } - - - @if (isLoading) { -
- -
- } - - - @if (!isLoading && totalElements === 0 && searchText?.trim()) { -
- search_off -

{{ 'iot-hub.no-items-found' | translate }}

-

{{ 'iot-hub.try-adjusting-search' | translate }}

-
- } - - - @if (!isLoading && resultGroups.length > 0) { -
- @for (group of resultGroups; track group.type) { -
- - - -
- @for (item of group.items; track item.id) { - - - } -
-
- } -
- } -
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts index f5c62c1bbc..fb69490e45 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts @@ -23,7 +23,7 @@ import { TbIotHubHomeComponent } from './iot-hub-home.component'; import { TbIotHubItemsPageComponent } from './iot-hub-items-page.component'; import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component'; import { TbIotHubInstalledItemsComponent } from './iot-hub-installed-items.component'; -import { TbIotHubSearchComponent } from './iot-hub-search.component'; +import { TbIotHubSearchPageComponent } from './iot-hub-search-page.component'; @NgModule({ declarations: [ @@ -31,7 +31,7 @@ import { TbIotHubSearchComponent } from './iot-hub-search.component'; TbIotHubItemsPageComponent, TbIotHubCreatorProfileComponent, TbIotHubInstalledItemsComponent, - TbIotHubSearchComponent + TbIotHubSearchPageComponent ], imports: [ CommonModule,