Browse Source

Refine IoT Hub widget select: filter panel state management, UI fixes

- Pending/applied filter state pattern: opening panel copies applied→pending,
  cancel discards pending, apply copies pending→applied
- Apply button disabled when no changes (dirty tracking via set comparison)
- Clear All button hidden when no filters checked
- Filter panel opens via tbPopoverVisibleChange, closes via popover.hide()
- Cancel button closes popover without applying
- Fix popover directive: use tb-popover attribute, tbPopover exportAs
- Fix search text propagation when switching All/Installed modes
- Add aria-hidden="false" on mat-icon with matBadge
- Rename RPC to Control in IoT Hub widget type translations
- Remove redundant tbPopoverShowCloseButton
- Filter panel style: 8px lr / 12px tb padding, 40px panel headers, 8px divider margin
pull/15508/head
Igor Kulikov 6 months ago
parent
commit
4259793756
  1. 10
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  2. 19
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html
  3. 14
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss
  4. 100
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts
  5. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json

10
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -457,17 +457,19 @@
</tb-toggle-select> </tb-toggle-select>
<button mat-icon-button type="button" <button mat-icon-button type="button"
*ngIf="dashboardWidgetSelectComponent?.selectWidgetMode === 'iotHub'" *ngIf="dashboardWidgetSelectComponent?.selectWidgetMode === 'iotHub'"
[tb-popover]="dashboardWidgetSelectComponent?.iotHubFilterPanel" tb-popover #iotHubFilterPopover="tbPopover"
[tbPopoverContent]="dashboardWidgetSelectComponent?.iotHubFilterPanel" [tbPopoverContent]="dashboardWidgetSelectComponent?.iotHubFilterPanel"
[tbPopoverContext]="{ popover: iotHubFilterPopover }"
tbPopoverTrigger="click" tbPopoverTrigger="click"
[tbPopoverOverlayStyle]="{maxWidth: '380px'}" (tbPopoverVisibleChange)="$event && dashboardWidgetSelectComponent?.openIotHubFilterPanel()"
tbPopoverShowCloseButton="true" [tbPopoverOverlayStyle]="{width: '380px'}"
matTooltip="{{ 'iot-hub.filter' | translate }}" matTooltip="{{ 'iot-hub.filter' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon [matBadge]="dashboardWidgetSelectComponent?.iotHubFilterCount || null" <mat-icon [matBadge]="dashboardWidgetSelectComponent?.iotHubFilterCount || null"
[matBadgeHidden]="!dashboardWidgetSelectComponent?.iotHubFilterCount" [matBadgeHidden]="!dashboardWidgetSelectComponent?.iotHubFilterCount"
matBadgeColor="warn" matBadgeColor="warn"
matBadgeSize="small">filter_list</mat-icon> matBadgeSize="small"
aria-hidden="false">filter_list</mat-icon>
</button> </button>
<tb-toggle-select *ngIf="dashboardWidgetSelectComponent?.selectWidgetMode === 'iotHub'" <tb-toggle-select *ngIf="dashboardWidgetSelectComponent?.selectWidgetMode === 'iotHub'"
appearance="fill-invert" appearance="fill-invert"

19
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html

@ -195,7 +195,7 @@
</ng-template> </ng-template>
<!-- IoT Hub Filter Panel --> <!-- IoT Hub Filter Panel -->
<ng-template #iotHubFilterPanel> <ng-template #iotHubFilterPanel let-popover="popover">
<div class="tb-iot-hub-filter-panel"> <div class="tb-iot-hub-filter-panel">
<div class="tb-iot-hub-filter-header"> <div class="tb-iot-hub-filter-header">
<span class="tb-iot-hub-filter-title">{{ 'iot-hub.filters' | translate }}</span> <span class="tb-iot-hub-filter-title">{{ 'iot-hub.filters' | translate }}</span>
@ -205,11 +205,11 @@
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
{{ 'iot-hub.type' | translate }} {{ 'iot-hub.type' | translate }}
<span *ngIf="iotHubActiveWidgetTypes.size" class="tb-iot-hub-filter-badge">{{ iotHubActiveWidgetTypes.size }}</span> <span *ngIf="iotHubPendingWidgetTypes.size" class="tb-iot-hub-filter-badge">{{ iotHubPendingWidgetTypes.size }}</span>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div *ngFor="let entry of iotHubWidgetTypesMap | keyvalue" class="tb-iot-hub-filter-option"> <div *ngFor="let entry of iotHubWidgetTypesMap | keyvalue" class="tb-iot-hub-filter-option">
<mat-checkbox [checked]="iotHubActiveWidgetTypes.has(entry.key)" <mat-checkbox [checked]="iotHubPendingWidgetTypes.has(entry.key)"
(change)="toggleIotHubWidgetType(entry.key)"> (change)="toggleIotHubWidgetType(entry.key)">
{{ entry.value | translate }} {{ entry.value | translate }}
</mat-checkbox> </mat-checkbox>
@ -220,11 +220,11 @@
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
{{ 'iot-hub.category' | translate }} {{ 'iot-hub.category' | translate }}
<span *ngIf="iotHubActiveCategories.size" class="tb-iot-hub-filter-badge">{{ iotHubActiveCategories.size }}</span> <span *ngIf="iotHubPendingCategories.size" class="tb-iot-hub-filter-badge">{{ iotHubPendingCategories.size }}</span>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div *ngFor="let entry of iotHubCategoriesMap | keyvalue" class="tb-iot-hub-filter-option"> <div *ngFor="let entry of iotHubCategoriesMap | keyvalue" class="tb-iot-hub-filter-option">
<mat-checkbox [checked]="iotHubActiveCategories.has(entry.key)" <mat-checkbox [checked]="iotHubPendingCategories.has(entry.key)"
(change)="toggleIotHubCategory(entry.key)"> (change)="toggleIotHubCategory(entry.key)">
{{ entry.value | translate }} {{ entry.value | translate }}
</mat-checkbox> </mat-checkbox>
@ -235,11 +235,11 @@
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
{{ 'iot-hub.use-case' | translate }} {{ 'iot-hub.use-case' | translate }}
<span *ngIf="iotHubActiveUseCases.size" class="tb-iot-hub-filter-badge">{{ iotHubActiveUseCases.size }}</span> <span *ngIf="iotHubPendingUseCases.size" class="tb-iot-hub-filter-badge">{{ iotHubPendingUseCases.size }}</span>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div *ngFor="let entry of iotHubUseCasesMap | keyvalue" class="tb-iot-hub-filter-option"> <div *ngFor="let entry of iotHubUseCasesMap | keyvalue" class="tb-iot-hub-filter-option">
<mat-checkbox [checked]="iotHubActiveUseCases.has(entry.key)" <mat-checkbox [checked]="iotHubPendingUseCases.has(entry.key)"
(change)="toggleIotHubUseCase(entry.key)"> (change)="toggleIotHubUseCase(entry.key)">
{{ entry.value | translate }} {{ entry.value | translate }}
</mat-checkbox> </mat-checkbox>
@ -247,9 +247,10 @@
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
<div class="tb-iot-hub-filter-actions"> <div class="tb-iot-hub-filter-actions">
<button mat-stroked-button (click)="clearIotHubFilters()">{{ 'iot-hub.clear-all' | translate }}</button> <button mat-stroked-button *ngIf="iotHubPendingWidgetTypes.size || iotHubPendingCategories.size || iotHubPendingUseCases.size" (click)="clearIotHubFilters()">{{ 'iot-hub.clear-all' | translate }}</button>
<span class="flex-1"></span> <span class="flex-1"></span>
<button mat-flat-button color="primary" (click)="applyIotHubFilters()">{{ 'action.apply' | translate }}</button> <button mat-button (click)="popover?.hide()">{{ 'action.cancel' | translate }}</button>
<button mat-flat-button color="primary" [disabled]="!iotHubFilterDirty" (click)="applyIotHubFilters(); popover?.hide()">{{ 'action.apply' | translate }}</button>
</div> </div>
</div> </div>
</ng-template> </ng-template>

14
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss

@ -202,7 +202,7 @@
// IoT Hub filter panel // IoT Hub filter panel
.tb-iot-hub-filter-panel { .tb-iot-hub-filter-panel {
padding: 24px; padding: 12px 8px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
@ -223,21 +223,25 @@
} }
.tb-iot-hub-filter-sections { .tb-iot-hub-filter-sections {
display: flex;
flex-direction: column;
overflow-y: auto; overflow-y: auto;
mat-expansion-panel { mat-expansion-panel {
box-shadow: none; box-shadow: none;
background: transparent; background: transparent;
&::ng-deep .mat-expansion-panel-body { .mat-expansion-panel-body {
padding: 0; padding: 0;
} }
.mat-expansion-panel-header {
padding: 8px 16px 8px 8px;
height: 40px;
min-height: 40px;
}
} }
mat-divider { mat-divider {
margin: 0; margin: 8px 0;
} }
} }

100
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts

@ -120,9 +120,9 @@ export class DashboardWidgetSelectComponent implements OnInit {
} else { } else {
this.iotHubInstalledMode = 'all'; this.iotHubInstalledMode = 'all';
this.installedWidgetVersions = null; this.installedWidgetVersions = null;
this.iotHubActiveWidgetTypes.clear(); this.appliedWidgetTypes.clear();
this.iotHubActiveCategories.clear(); this.appliedCategories.clear();
this.iotHubActiveUseCases.clear(); this.appliedUseCases.clear();
this.iotHubFilterCount = 0; this.iotHubFilterCount = 0;
} }
} }
@ -196,10 +196,17 @@ export class DashboardWidgetSelectComponent implements OnInit {
private installedWidgetVersions: MpItemVersionView[] = null; private installedWidgetVersions: MpItemVersionView[] = null;
iotHubInstalledWidgetsFilter = ''; iotHubInstalledWidgetsFilter = '';
// IoT Hub filter model // IoT Hub filter model — applied state (used by fetch functions)
iotHubActiveWidgetTypes = new Set<string>(); private appliedWidgetTypes = new Set<string>();
iotHubActiveCategories = new Set<string>(); private appliedCategories = new Set<string>();
iotHubActiveUseCases = new Set<string>(); private appliedUseCases = new Set<string>();
// IoT Hub filter model — pending state (used by filter panel UI)
iotHubPendingWidgetTypes = new Set<string>();
iotHubPendingCategories = new Set<string>();
iotHubPendingUseCases = new Set<string>();
iotHubFilterDirty = false;
iotHubWidgetTypesMap: Map<string, string> = iotHubWidgetTypeTranslations; iotHubWidgetTypesMap: Map<string, string> = iotHubWidgetTypeTranslations;
iotHubCategoriesMap: Map<string, string> = getCategoriesForType(ItemType.WIDGET); iotHubCategoriesMap: Map<string, string> = getCategoriesForType(ItemType.WIDGET);
iotHubUseCasesMap: Map<string, string> = useCaseTranslations as Map<string, string>; iotHubUseCasesMap: Map<string, string> = useCaseTranslations as Map<string, string>;
@ -242,10 +249,10 @@ export class DashboardWidgetSelectComponent implements OnInit {
const pageLink = new PageLink(pageSize, page, search || null, sortOrder); const pageLink = new PageLink(pageSize, page, search || null, sortOrder);
const query = new MpItemVersionQuery(pageLink, ItemType.WIDGET, const query = new MpItemVersionQuery(pageLink, ItemType.WIDGET,
undefined, undefined, undefined, undefined,
this.iotHubActiveCategories.size > 0 ? Array.from(this.iotHubActiveCategories) : undefined, this.appliedCategories.size > 0 ? Array.from(this.appliedCategories) : undefined,
this.iotHubActiveUseCases.size > 0 ? Array.from(this.iotHubActiveUseCases) : undefined, this.appliedUseCases.size > 0 ? Array.from(this.appliedUseCases) : undefined,
undefined, undefined,
this.iotHubActiveWidgetTypes.size > 0 ? Array.from(this.iotHubActiveWidgetTypes) : undefined this.appliedWidgetTypes.size > 0 ? Array.from(this.appliedWidgetTypes) : undefined
); );
return this.iotHubApiService.getPublishedVersions(query, { ignoreLoading: true }); return this.iotHubApiService.getPublishedVersions(query, { ignoreLoading: true });
}; };
@ -375,48 +382,81 @@ export class DashboardWidgetSelectComponent implements OnInit {
onIotHubInstalledModeChange(mode: 'all' | 'installed'): void { onIotHubInstalledModeChange(mode: 'all' | 'installed'): void {
this.iotHubInstalledMode = mode; this.iotHubInstalledMode = mode;
const search = this.searchSubject.value;
if (mode === 'installed') { if (mode === 'installed') {
this.installedWidgetVersions = null; this.installedWidgetVersions = null;
this.iotHubInstalledWidgetsFilter = search;
} else {
this.iotHubWidgetsFilter = search;
} }
this.cd.markForCheck(); this.cd.markForCheck();
} }
openIotHubFilterPanel(): void {
this.iotHubPendingWidgetTypes = new Set(this.appliedWidgetTypes);
this.iotHubPendingCategories = new Set(this.appliedCategories);
this.iotHubPendingUseCases = new Set(this.appliedUseCases);
this.iotHubFilterDirty = false;
}
toggleIotHubWidgetType(key: string): void { toggleIotHubWidgetType(key: string): void {
if (this.iotHubActiveWidgetTypes.has(key)) { if (this.iotHubPendingWidgetTypes.has(key)) {
this.iotHubActiveWidgetTypes.delete(key); this.iotHubPendingWidgetTypes.delete(key);
} else { } else {
this.iotHubActiveWidgetTypes.add(key); this.iotHubPendingWidgetTypes.add(key);
} }
this.updateFilterDirty();
} }
toggleIotHubCategory(key: string): void { toggleIotHubCategory(key: string): void {
if (this.iotHubActiveCategories.has(key)) { if (this.iotHubPendingCategories.has(key)) {
this.iotHubActiveCategories.delete(key); this.iotHubPendingCategories.delete(key);
} else { } else {
this.iotHubActiveCategories.add(key); this.iotHubPendingCategories.add(key);
} }
this.updateFilterDirty();
} }
toggleIotHubUseCase(key: string): void { toggleIotHubUseCase(key: string): void {
if (this.iotHubActiveUseCases.has(key)) { if (this.iotHubPendingUseCases.has(key)) {
this.iotHubActiveUseCases.delete(key); this.iotHubPendingUseCases.delete(key);
} else { } else {
this.iotHubActiveUseCases.add(key); this.iotHubPendingUseCases.add(key);
} }
this.updateFilterDirty();
} }
clearIotHubFilters(): void { clearIotHubFilters(): void {
this.iotHubActiveWidgetTypes.clear(); this.iotHubPendingWidgetTypes.clear();
this.iotHubActiveCategories.clear(); this.iotHubPendingCategories.clear();
this.iotHubActiveUseCases.clear(); this.iotHubPendingUseCases.clear();
this.applyIotHubFilters(); this.updateFilterDirty();
} }
applyIotHubFilters(): void { applyIotHubFilters(): void {
this.iotHubFilterCount = this.iotHubActiveWidgetTypes.size + this.iotHubActiveCategories.size + this.iotHubActiveUseCases.size; this.appliedWidgetTypes = new Set(this.iotHubPendingWidgetTypes);
this.appliedCategories = new Set(this.iotHubPendingCategories);
this.appliedUseCases = new Set(this.iotHubPendingUseCases);
this.iotHubFilterCount = this.appliedWidgetTypes.size + this.appliedCategories.size + this.appliedUseCases.size;
this.iotHubFilterDirty = false;
this.reloadIotHubWidgets(); this.reloadIotHubWidgets();
} }
private updateFilterDirty(): void {
this.iotHubFilterDirty =
!this.setsEqual(this.iotHubPendingWidgetTypes, this.appliedWidgetTypes) ||
!this.setsEqual(this.iotHubPendingCategories, this.appliedCategories) ||
!this.setsEqual(this.iotHubPendingUseCases, this.appliedUseCases);
}
private setsEqual(a: Set<string>, b: Set<string>): boolean {
if (a.size !== b.size) { return false; }
for (const v of a) {
if (!b.has(v)) { return false; }
}
return true;
}
private reloadIotHubWidgets(): void { private reloadIotHubWidgets(): void {
if (this.iotHubInstalledMode === 'installed') { if (this.iotHubInstalledMode === 'installed') {
this.installedWidgetVersions = null; this.installedWidgetVersions = null;
@ -489,14 +529,14 @@ export class DashboardWidgetSelectComponent implements OnInit {
if (search) { if (search) {
filtered = filtered.filter(v => v.name.toLowerCase().includes(search.toLowerCase())); filtered = filtered.filter(v => v.name.toLowerCase().includes(search.toLowerCase()));
} }
if (this.iotHubActiveWidgetTypes.size > 0) { if (this.appliedWidgetTypes.size > 0) {
filtered = filtered.filter(v => this.iotHubActiveWidgetTypes.has(v.dataDescriptor?.widgetType)); filtered = filtered.filter(v => this.appliedWidgetTypes.has(v.dataDescriptor?.widgetType));
} }
if (this.iotHubActiveCategories.size > 0) { if (this.appliedCategories.size > 0) {
filtered = filtered.filter(v => v.categories?.some(c => this.iotHubActiveCategories.has(c))); filtered = filtered.filter(v => v.categories?.some(c => this.appliedCategories.has(c)));
} }
if (this.iotHubActiveUseCases.size > 0) { if (this.appliedUseCases.size > 0) {
filtered = filtered.filter(v => v.useCases?.some(u => this.iotHubActiveUseCases.has(u))); filtered = filtered.filter(v => v.useCases?.some(u => this.appliedUseCases.has(u)));
} }
const start = page * pageSize; const start = page * pageSize;
const data = filtered.slice(start, start + pageSize); const data = filtered.slice(start, start + pageSize);

2
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3898,7 +3898,7 @@
"item-data": { "item-data": {
"widget-type-timeseries": "Timeseries", "widget-type-timeseries": "Timeseries",
"widget-type-latest": "Latest", "widget-type-latest": "Latest",
"widget-type-rpc": "RPC", "widget-type-rpc": "Control",
"widget-type-alarm": "Alarm", "widget-type-alarm": "Alarm",
"widget-type-static": "Static", "widget-type-static": "Static",
"cf-type-simple": "Simple", "cf-type-simple": "Simple",

Loading…
Cancel
Save