From 536f980fe45fecde633b0432d7ce95368eaa4cf8 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 11 Jul 2026 12:55:16 +0200 Subject: [PATCH 1/2] fix(ui): sort edge components by alias --- .../shared/components/edge/edgeconfig.spec.ts | 16 ++++++++++++++++ ui/src/app/shared/components/edge/edgeconfig.ts | 5 +++++ .../app/shared/utils/array/array.utils.spec.ts | 10 ++++++++++ ui/src/app/shared/utils/array/array.utils.ts | 1 + 4 files changed, 32 insertions(+) diff --git a/ui/src/app/shared/components/edge/edgeconfig.spec.ts b/ui/src/app/shared/components/edge/edgeconfig.spec.ts index 2d3a64738ad..559d60e8612 100644 --- a/ui/src/app/shared/components/edge/edgeconfig.spec.ts +++ b/ui/src/app/shared/components/edge/edgeconfig.spec.ts @@ -11,6 +11,22 @@ import { OeChartTester, OeFormlyViewTester } from "../shared/testing/tester"; import { Edge } from "./edge"; import { EdgeConfig, PersistencePriority } from "./edgeconfig"; +describe("EdgeConfig component ordering", () => { + it("sorts components returned by a factory by alias", () => { + const config = DummyConfig.from( + DummyConfig.Component.SOCOMEC_CONSUMPTION_METER("meter0", "Meter 10"), + DummyConfig.Component.SOCOMEC_CONSUMPTION_METER("meter1", ""), + DummyConfig.Component.SOCOMEC_CONSUMPTION_METER("meter2", "Meter 2"), + ); + + expect(config.getComponentsByFactory("Meter.Socomec.Threephase").map(component => component.alias)).toEqual([ + "Meter 2", + "Meter 10", + "", + ]); + }); +}); + export namespace DummyConfig { export function dummyEdge(values: { edgeId?: string; diff --git a/ui/src/app/shared/components/edge/edgeconfig.ts b/ui/src/app/shared/components/edge/edgeconfig.ts index cbdac8b8830..5b2c5ecf66c 100644 --- a/ui/src/app/shared/components/edge/edgeconfig.ts +++ b/ui/src/app/shared/components/edge/edgeconfig.ts @@ -1,6 +1,7 @@ import { TranslateService } from "@ngx-translate/core"; import { ChannelAddress } from "../../shared"; import { Widgets } from "../../type/widgets"; +import { ArrayUtils } from "../../utils/array/array.utils"; import { Edge } from "./edge"; export interface CategorizedComponents { @@ -106,6 +107,10 @@ export class EdgeConfig { // Complete 'factories' map factory.componentIds.push(componentId); } + + for (const factory of Object.values(this.factories)) { + ArrayUtils.sortedAlphabetically(factory.componentIds, componentId => this.components[componentId].alias); + } } // Initialize Widgets diff --git a/ui/src/app/shared/utils/array/array.utils.spec.ts b/ui/src/app/shared/utils/array/array.utils.spec.ts index 9e5f64f9a48..ead6d1a042f 100644 --- a/ui/src/app/shared/utils/array/array.utils.spec.ts +++ b/ui/src/app/shared/utils/array/array.utils.spec.ts @@ -25,6 +25,16 @@ describe("Array-Utils", () => { expect(() => ArrayUtils.sortedAlphabetically(inputArr, null)).toThrow(); }); + it("sorts numeric suffixes naturally", () => { + const inputArr = ["Meter 10", "Meter 2", "Meter 1"]; + + expect(ArrayUtils.sortedAlphabetically(inputArr, a => a)).toEqual([ + "Meter 1", + "Meter 2", + "Meter 10", + ]); + }); + describe("ReducerFunctions", () => { it("+sum", () => { diff --git a/ui/src/app/shared/utils/array/array.utils.ts b/ui/src/app/shared/utils/array/array.utils.ts index 89efaa36a6d..c3f97c10a9b 100644 --- a/ui/src/app/shared/utils/array/array.utils.ts +++ b/ui/src/app/shared/utils/array/array.utils.ts @@ -84,6 +84,7 @@ export namespace ArrayUtils { } return aVal.localeCompare(bVal, undefined, { sensitivity: "accent", + numeric: true, }); }); } From 7e02d889a9704098c6ffed105703cb0378f547ef Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 11 Jul 2026 13:05:15 +0200 Subject: [PATCH 2/2] fix(ui): preserve alias ordering in component lists --- .../shared/components/edge/edgeconfig.spec.ts | 34 +++++++++++++++++++ .../app/shared/components/edge/edgeconfig.ts | 6 ++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/ui/src/app/shared/components/edge/edgeconfig.spec.ts b/ui/src/app/shared/components/edge/edgeconfig.spec.ts index 559d60e8612..9afd455fa3a 100644 --- a/ui/src/app/shared/components/edge/edgeconfig.spec.ts +++ b/ui/src/app/shared/components/edge/edgeconfig.spec.ts @@ -1,4 +1,5 @@ // @ts-strict-ignore +import { TranslateService } from "@ngx-translate/core"; import { TimeUnit } from "chart.js"; import { SumState } from "src/app/index/shared/sumState"; import { ChartConstants } from "src/app/shared/components/chart/chart.constants"; @@ -12,6 +13,8 @@ import { Edge } from "./edge"; import { EdgeConfig, PersistencePriority } from "./edgeconfig"; describe("EdgeConfig component ordering", () => { + const translate = { instant: (key: string) => key } as TranslateService; + it("sorts components returned by a factory by alias", () => { const config = DummyConfig.from( DummyConfig.Component.SOCOMEC_CONSUMPTION_METER("meter0", "Meter 10"), @@ -25,6 +28,37 @@ describe("EdgeConfig component ordering", () => { "", ]); }); + + it("sorts components across factories when listed by nature", () => { + const nullAlias = DummyConfig.Component.GOODWE_GRID_METER("goodwe1", "Meter 1"); + nullAlias.alias = null as unknown as string; + const config = DummyConfig.from( + DummyConfig.Component.GOODWE_GRID_METER("goodwe0", "Meter 10"), + nullAlias, + DummyConfig.Component.SOCOMEC_CONSUMPTION_METER("meter0", "Meter 2"), + DummyConfig.Component.SOCOMEC_CONSUMPTION_METER("meter1", ""), + ); + + const aliases = config.getComponentsImplementingNature("io.openems.edge.meter.api.ElectricityMeter").map(component => component.alias); + expect(aliases.slice(0, 2)).toEqual(["Meter 2", "Meter 10"]); + expect(aliases.slice(2)).toEqual(jasmine.arrayWithExactContents(["", null])); + }); + + it("sorts active components by alias instead of component ID", () => { + const nullAlias = DummyConfig.Component.GOODWE_GRID_METER("goodwe1", "Meter 1"); + nullAlias.alias = null as unknown as string; + const config = DummyConfig.from( + DummyConfig.Component.GOODWE_GRID_METER("goodwe0", "Meter 10"), + nullAlias, + DummyConfig.Component.SOCOMEC_CONSUMPTION_METER("meter0", "Meter 2"), + DummyConfig.Component.SOCOMEC_CONSUMPTION_METER("meter1", ""), + ); + + const meterCategory = config.listActiveComponents([], translate).find(category => category.components.length > 0); + const aliases = meterCategory?.components.map(component => component.alias); + expect(aliases?.slice(0, 2)).toEqual(["Meter 2", "Meter 10"]); + expect(aliases?.slice(2)).toEqual(jasmine.arrayWithExactContents(["", null])); + }); }); export namespace DummyConfig { diff --git a/ui/src/app/shared/components/edge/edgeconfig.ts b/ui/src/app/shared/components/edge/edgeconfig.ts index 5b2c5ecf66c..9fa863f9425 100644 --- a/ui/src/app/shared/components/edge/edgeconfig.ts +++ b/ui/src/app/shared/components/edge/edgeconfig.ts @@ -480,6 +480,7 @@ export class EdgeConfig { result.push(...this.getComponentsImplementingNature("io.openems.edge.meter.api.SymmetricMeter")); } + ArrayUtils.sortedAlphabetically(result, component => component.alias); return result; } @@ -708,9 +709,8 @@ export class EdgeConfig { // remove Components from list that have already been listed before .filter(component => !ignoreComponentIds.includes(component.id)) // remove duplicates - .filter((e, i, arr) => arr.indexOf(e) === i) - // sort by ID - .sort((c1, c2) => c1.id.localeCompare(c2.id)); + .filter((e, i, arr) => arr.indexOf(e) === i); + ArrayUtils.sortedAlphabetically(components, component => component.alias); if (components.length > 0) { components.forEach(component => { ignoreComponentIds.push(component.id);