diff --git a/assets/js/components/Issue/format.test.ts b/assets/js/components/Issue/format.test.ts index 1ce46aed7..70dc81d0b 100644 --- a/assets/js/components/Issue/format.test.ts +++ b/assets/js/components/Issue/format.test.ts @@ -2,20 +2,32 @@ import { describe, it, expect } from "vitest"; import { formatJson } from "./format"; describe("formatJson", () => { - it("formats basic object", () => { + it("formats basic object with sorted keys", () => { const obj = { foo: "bar", baz: 123 }; const result = formatJson(obj); expect(result).toBe(`{ - "foo": "bar", - "baz": 123 + "baz": 123, + "foo": "bar" +}`); + }); + + it("sorts id and name first", () => { + const obj = { zebra: 1, name: "test", id: 42, alpha: 2 }; + const result = formatJson(obj); + + expect(result).toBe(`{ + "id": 42, + "name": "test", + "alpha": 2, + "zebra": 1 }`); }); it("expands arrays with expand keys", () => { const obj = { - items: ["foo", "bar"], other: ["baz", "qux"], + items: ["foo", "bar"], }; const result = formatJson(obj, ["items"]); @@ -30,8 +42,8 @@ describe("formatJson", () => { it("expands objects with expand keys", () => { const obj = { - config: { alpha: 1, beta: 2 }, other: { gamma: 3 }, + config: { beta: 2, alpha: 1 }, }; const result = formatJson(obj, ["config"]); @@ -44,19 +56,19 @@ describe("formatJson", () => { }`); }); - it("keeps nested objects in arrays single-lined", () => { + it("keeps nested objects in arrays single-lined with sorted keys", () => { const obj = { items: [ { foo: "bar", baz: 123 }, - { qux: "test", num: 456 }, + { qux: "test", id: 1, num: 456 }, ], }; const result = formatJson(obj, ["items"]); expect(result).toBe(`{ "items": [ - {"foo":"bar","baz":123}, - {"qux":"test","num":456} + {"baz":123,"foo":"bar"}, + {"id":1,"num":456,"qux":"test"} ] }`); }); diff --git a/assets/js/components/Issue/format.ts b/assets/js/components/Issue/format.ts index 019dfcc0f..9024f1df1 100644 --- a/assets/js/components/Issue/format.ts +++ b/assets/js/components/Issue/format.ts @@ -1,3 +1,15 @@ +function sortedEntries(obj: object): [string, any][] { + const firstKeys = ["id", "name"]; + return Object.entries(obj).sort(([a], [b]) => { + const ai = firstKeys.indexOf(a); + const bi = firstKeys.indexOf(b); + if (ai >= 0 || bi >= 0) { + return (ai >= 0 ? ai : Infinity) - (bi >= 0 ? bi : Infinity); + } + return a.localeCompare(b); + }); +} + export function formatJson(obj: any, expandKeys: string[] = []): string { if (!obj || typeof obj !== "object") { return JSON.stringify(obj, null, 2); @@ -5,7 +17,7 @@ export function formatJson(obj: any, expandKeys: string[] = []): string { const lines: string[] = []; - for (const [key, value] of Object.entries(obj)) { + for (const [key, value] of sortedEntries(obj)) { let valueStr: string; // Check if this key should be expanded (only if not empty) @@ -19,14 +31,17 @@ export function formatJson(obj: any, expandKeys: string[] = []): string { valueStr = "[]"; } else { const arrayItems = value.map((item) => { - const itemStr = JSON.stringify(item); + const itemStr = + item && typeof item === "object" && !Array.isArray(item) + ? JSON.stringify(Object.fromEntries(sortedEntries(item))) + : JSON.stringify(item); return ` ${itemStr.replace(/\\n/g, "\n")}`; }); valueStr = `[\n${arrayItems.join(",\n")}\n ]`; } } else { // Object expansion - const objEntries = Object.entries(value); + const objEntries = sortedEntries(value); if (objEntries.length === 0) { // Keep empty objects compact valueStr = "{}"; diff --git a/assets/js/views/Issue.vue b/assets/js/views/Issue.vue index 9243d9461..f2c646b90 100644 --- a/assets/js/views/Issue.vue +++ b/assets/js/views/Issue.vue @@ -330,14 +330,13 @@ import type { State } from "@/types/evcc"; // Keys that should be expanded (1-level expansion for arrays and objects) const EXPAND_KEYS = [ "battery", + "charger", "forecast", "loadpoints", - "pv", - "vehicles", - "statistics", - "charger", - "site", + "messenger", "meter", + "pv", + "tariff", "vehicle", ]; @@ -494,7 +493,9 @@ export default defineComponent({ const deviceEndpoints = [ "config/loadpoints", "config/devices/charger", + "config/devices/messenger", "config/devices/meter", + "config/devices/tariff", "config/devices/vehicle", ]; @@ -505,7 +506,9 @@ export default defineComponent({ "config/eebus", "config/hems", "config/messaging", + "config/messagingEvents", "config/tariffs", + "config/tariff", ]; const configs: any = {}; @@ -515,7 +518,11 @@ export default defineComponent({ // Add private=false for device endpoints to hide private data in bug reports const response = await api.get(endpoint, { params: { private: false } }); if (response.data && Object.keys(response.data).length > 0) { - const key = endpoint.replace("config/", "").replace("devices/", ""); + let key = endpoint.replace("config/", "").replace("devices/", ""); + // avoid collision with config/devices/tariff + if (key === "tariff" && !deviceEndpoints.includes(endpoint)) { + key = "tariffRefs"; + } let data = response.data; // Filter out entries without id property for device endpoints