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

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

@ -195,7 +195,7 @@
</ng-template>
<!-- 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-header">
<span class="tb-iot-hub-filter-title">{{ 'iot-hub.filters' | translate }}</span>
@ -205,11 +205,11 @@
<mat-expansion-panel-header>
<mat-panel-title>
{{ '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-expansion-panel-header>
<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)">
{{ entry.value | translate }}
</mat-checkbox>
@ -220,11 +220,11 @@
<mat-expansion-panel-header>
<mat-panel-title>
{{ '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-expansion-panel-header>
<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)">
{{ entry.value | translate }}
</mat-checkbox>
@ -235,11 +235,11 @@
<mat-expansion-panel-header>
<mat-panel-title>
{{ '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-expansion-panel-header>
<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)">
{{ entry.value | translate }}
</mat-checkbox>
@ -247,9 +247,10 @@
</mat-expansion-panel>
</div>
<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>
<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>
</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
.tb-iot-hub-filter-panel {
padding: 24px;
padding: 12px 8px;
display: flex;
flex-direction: column;
gap: 12px;
@ -223,21 +223,25 @@
}
.tb-iot-hub-filter-sections {
display: flex;
flex-direction: column;
overflow-y: auto;
mat-expansion-panel {
box-shadow: none;
background: transparent;
&::ng-deep .mat-expansion-panel-body {
.mat-expansion-panel-body {
padding: 0;
}
.mat-expansion-panel-header {
padding: 8px 16px 8px 8px;
height: 40px;
min-height: 40px;
}
}
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 {
this.iotHubInstalledMode = 'all';
this.installedWidgetVersions = null;
this.iotHubActiveWidgetTypes.clear();
this.iotHubActiveCategories.clear();
this.iotHubActiveUseCases.clear();
this.appliedWidgetTypes.clear();
this.appliedCategories.clear();
this.appliedUseCases.clear();
this.iotHubFilterCount = 0;
}
}
@ -196,10 +196,17 @@ export class DashboardWidgetSelectComponent implements OnInit {
private installedWidgetVersions: MpItemVersionView[] = null;
iotHubInstalledWidgetsFilter = '';
// IoT Hub filter model
iotHubActiveWidgetTypes = new Set<string>();
iotHubActiveCategories = new Set<string>();
iotHubActiveUseCases = new Set<string>();
// IoT Hub filter model — applied state (used by fetch functions)
private appliedWidgetTypes = new Set<string>();
private appliedCategories = 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;
iotHubCategoriesMap: Map<string, string> = getCategoriesForType(ItemType.WIDGET);
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 query = new MpItemVersionQuery(pageLink, ItemType.WIDGET,
undefined, undefined,
this.iotHubActiveCategories.size > 0 ? Array.from(this.iotHubActiveCategories) : undefined,
this.iotHubActiveUseCases.size > 0 ? Array.from(this.iotHubActiveUseCases) : undefined,
this.appliedCategories.size > 0 ? Array.from(this.appliedCategories) : undefined,
this.appliedUseCases.size > 0 ? Array.from(this.appliedUseCases) : 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 });
};
@ -375,48 +382,81 @@ export class DashboardWidgetSelectComponent implements OnInit {
onIotHubInstalledModeChange(mode: 'all' | 'installed'): void {
this.iotHubInstalledMode = mode;
const search = this.searchSubject.value;
if (mode === 'installed') {
this.installedWidgetVersions = null;
this.iotHubInstalledWidgetsFilter = search;
} else {
this.iotHubWidgetsFilter = search;
}
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 {
if (this.iotHubActiveWidgetTypes.has(key)) {
this.iotHubActiveWidgetTypes.delete(key);
if (this.iotHubPendingWidgetTypes.has(key)) {
this.iotHubPendingWidgetTypes.delete(key);
} else {
this.iotHubActiveWidgetTypes.add(key);
this.iotHubPendingWidgetTypes.add(key);
}
this.updateFilterDirty();
}
toggleIotHubCategory(key: string): void {
if (this.iotHubActiveCategories.has(key)) {
this.iotHubActiveCategories.delete(key);
if (this.iotHubPendingCategories.has(key)) {
this.iotHubPendingCategories.delete(key);
} else {
this.iotHubActiveCategories.add(key);
this.iotHubPendingCategories.add(key);
}
this.updateFilterDirty();
}
toggleIotHubUseCase(key: string): void {
if (this.iotHubActiveUseCases.has(key)) {
this.iotHubActiveUseCases.delete(key);
if (this.iotHubPendingUseCases.has(key)) {
this.iotHubPendingUseCases.delete(key);
} else {
this.iotHubActiveUseCases.add(key);
this.iotHubPendingUseCases.add(key);
}
this.updateFilterDirty();
}
clearIotHubFilters(): void {
this.iotHubActiveWidgetTypes.clear();
this.iotHubActiveCategories.clear();
this.iotHubActiveUseCases.clear();
this.applyIotHubFilters();
this.iotHubPendingWidgetTypes.clear();
this.iotHubPendingCategories.clear();
this.iotHubPendingUseCases.clear();
this.updateFilterDirty();
}
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();
}
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 {
if (this.iotHubInstalledMode === 'installed') {
this.installedWidgetVersions = null;
@ -489,14 +529,14 @@ export class DashboardWidgetSelectComponent implements OnInit {
if (search) {
filtered = filtered.filter(v => v.name.toLowerCase().includes(search.toLowerCase()));
}
if (this.iotHubActiveWidgetTypes.size > 0) {
filtered = filtered.filter(v => this.iotHubActiveWidgetTypes.has(v.dataDescriptor?.widgetType));
if (this.appliedWidgetTypes.size > 0) {
filtered = filtered.filter(v => this.appliedWidgetTypes.has(v.dataDescriptor?.widgetType));
}
if (this.iotHubActiveCategories.size > 0) {
filtered = filtered.filter(v => v.categories?.some(c => this.iotHubActiveCategories.has(c)));
if (this.appliedCategories.size > 0) {
filtered = filtered.filter(v => v.categories?.some(c => this.appliedCategories.has(c)));
}
if (this.iotHubActiveUseCases.size > 0) {
filtered = filtered.filter(v => v.useCases?.some(u => this.iotHubActiveUseCases.has(u)));
if (this.appliedUseCases.size > 0) {
filtered = filtered.filter(v => v.useCases?.some(u => this.appliedUseCases.has(u)));
}
const start = page * 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": {
"widget-type-timeseries": "Timeseries",
"widget-type-latest": "Latest",
"widget-type-rpc": "RPC",
"widget-type-rpc": "Control",
"widget-type-alarm": "Alarm",
"widget-type-static": "Static",
"cf-type-simple": "Simple",

Loading…
Cancel
Save