fix(mqtt): resolve console labels at render time
This commit is contained in:
1 parent
801a96a003
commit
8d084cc655
9 files changed
+296
-3
No files matched your search
@@ -369,7 +369,7 @@ onUnmounted(stopPolling);
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-xs font-medium">
|
||||
<input v-model="formNoLocal" type="checkbox" :disabled="savingSubscription || !mqtt5" />
|
||||
<span>{{ t("connection.mqttNoLocal") }}(No Local)</span>
|
||||
<span>{{ t("connection.mqttNoLocal") }}</span>
|
||||
<span v-if="!mqtt5" class="font-normal text-muted-foreground">{{ t("connection.mqttNoLocalMqtt5Only") }}</span>
|
||||
</label>
|
||||
<label class="flex items-start gap-2 text-xs font-medium">
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const source = readFileSync(new URL("../MqttAdminConsole.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("MqttAdminConsole i18n regression guards", () => {
|
||||
it("does not append a hardcoded (No Local) suffix to the No Local label", () => {
|
||||
// Issue #5858 follow-up: the subscription dialog used to render
|
||||
// `t("connection.mqttNoLocal")(No Local)`. The CJK-bracket suffix was
|
||||
// locale-invariant and leaked Chinese punctuation into every language.
|
||||
expect(source).not.toContain(")No Local)");
|
||||
expect(source).not.toContain("(No Local)");
|
||||
});
|
||||
|
||||
it("renders the No Local label purely through vue-i18n", () => {
|
||||
// The en locale value for connection.mqttNoLocal is already "No Local", so
|
||||
// dropping the hardcoded suffix loses no information. Guard the intent.
|
||||
expect(source).toContain('t("connection.mqttNoLocal")');
|
||||
expect(source).not.toContain('t("connection.mqttNoLocal")(No Local)');
|
||||
});
|
||||
});
|
||||
@@ -350,6 +350,7 @@ function displayLabel(node: TreeNode): string {
|
||||
if (node.type === "object-browser") return t(node.label, { count: node.objectCount ?? 0 });
|
||||
if (node.type === "user-admin" || node.type === "dameng-job-admin") return t(node.label);
|
||||
if (node.type === "linked-server-root") return t(node.label);
|
||||
if (node.type === "mqtt-topic" && node.id.endsWith(":mqtt-topic:__console__")) return t(node.label);
|
||||
if (node.label === "tree.defaultDatabase") return t(node.label);
|
||||
return isGroupLabel(node) ? t(node.label) : node.label;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick, type App } from "vue";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n, { setLocale } from "@/i18n";
|
||||
import TreeItem from "@/components/sidebar/TreeItem.vue";
|
||||
import { createSidebarTreeRuntime, sidebarTreeRuntimeKey, type SidebarTreeRuntimeHost } from "@/lib/sidebar/sidebarTreeRuntime";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
|
||||
const connectionStore = {
|
||||
activeConnectionId: "mqtt-1",
|
||||
connectedIds: new Set(["mqtt-1"]),
|
||||
connectingIds: new Set<string>(),
|
||||
connectionMultiSelectActive: false,
|
||||
connections: [],
|
||||
getConfig: () => ({ id: "mqtt-1", name: "test-mqtt", db_type: "mqtt" }),
|
||||
isDefaultDatabase: () => false,
|
||||
isDefaultSchema: () => false,
|
||||
isPinnedTreeNodeReorderTarget: () => false,
|
||||
isTreeNodeChildrenLoaded: () => false,
|
||||
isTreeNodePinned: () => false,
|
||||
selectedTreeNodeId: null as string | null,
|
||||
selectedTreeNodeIds: [] as string[],
|
||||
selectedTreeNodeIdsSet: new Set<string>(),
|
||||
sidebarTableSearchQueries: {},
|
||||
tableNameFilterForScope: () => undefined,
|
||||
treeNodes: [],
|
||||
treeSelectionAnchorId: null as string | null,
|
||||
};
|
||||
|
||||
vi.mock("@/stores/connectionStore", () => ({
|
||||
useConnectionStore: () => connectionStore,
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/queryStore", () => ({
|
||||
useQueryStore: () => ({ openDatabaseKeys: new Set<string>() }),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/settingsStore", () => ({
|
||||
useSettingsStore: () => ({
|
||||
editorSettings: {
|
||||
shortcuts: { openDataInNewTab: "" },
|
||||
sidebarActivation: "double",
|
||||
sidebarAllowHorizontalScroll: false,
|
||||
sidebarHiddenTablePrefixes: [],
|
||||
sidebarObjectInfoMode: "none",
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/composables/useToast", () => ({
|
||||
useToast: () => ({ toast: vi.fn() }),
|
||||
}));
|
||||
|
||||
const mountedApps: App[] = [];
|
||||
|
||||
function runtimeHost(): SidebarTreeRuntimeHost {
|
||||
return {
|
||||
buildContextMenu: vi.fn(() => []),
|
||||
handleRowClick: vi.fn(),
|
||||
handleRowDoubleClick: vi.fn(),
|
||||
handleRowKeydown: vi.fn(),
|
||||
openPrimaryVisibleFilter: vi.fn(),
|
||||
openDataInNewTab: vi.fn(),
|
||||
requestPaste: vi.fn(() => false),
|
||||
toggleNode: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
async function mountMqttConsoleNode() {
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
const node: TreeNode = {
|
||||
id: "mqtt-1:mqtt-topic:__console__",
|
||||
label: "connection.mqttConsoleTitle",
|
||||
type: "mqtt-topic",
|
||||
connectionId: "mqtt-1",
|
||||
children: [],
|
||||
};
|
||||
const runtime = createSidebarTreeRuntime();
|
||||
runtime.bindHost(runtimeHost());
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup: () => () => h(TreeItem, { node, depth: 1 }),
|
||||
}),
|
||||
);
|
||||
mountedApps.push(app);
|
||||
app.use(i18n);
|
||||
app.provide(sidebarTreeRuntimeKey, runtime);
|
||||
app.mount(container);
|
||||
await nextTick();
|
||||
|
||||
const row = container.querySelector<HTMLElement>("[tabindex]");
|
||||
if (!row) throw new Error(`MQTT console row was not rendered: ${container.innerHTML}`);
|
||||
return row;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
for (const app of mountedApps.splice(0)) app.unmount();
|
||||
document.body.replaceChildren();
|
||||
await setLocale("en");
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("TreeItem MQTT console i18n", () => {
|
||||
it("updates the existing synthetic node when the locale changes", async () => {
|
||||
await setLocale("en");
|
||||
const row = await mountMqttConsoleNode();
|
||||
|
||||
expect(row.textContent).toContain("MQTT Console");
|
||||
|
||||
await setLocale("zh-CN");
|
||||
await nextTick();
|
||||
|
||||
expect(row.textContent).toContain("MQTT 控制台");
|
||||
expect(row.textContent).not.toContain("MQTT Console");
|
||||
});
|
||||
});
|
||||
@@ -127,6 +127,9 @@ export function tabDisplayTitle(tab: QueryTab, t: Translate): string {
|
||||
if (compact) return connectionDisplayName(tab.connectionId);
|
||||
return `${connectionDisplayName(tab.connectionId)}@${t("consul.ui.overview")}`;
|
||||
}
|
||||
if (tab.mode === "mqtt") {
|
||||
return `${connectionDisplayName(tab.connectionId)} - ${t("connection.mqttConsoleTitle")}`;
|
||||
}
|
||||
if (tab.mode === "objects") {
|
||||
const schema = tab.objectBrowser?.schema;
|
||||
if (compact) return schema || tab.title;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ConnectionConfig, TreeNode } from "@/types/database";
|
||||
|
||||
function installLocalStorage() {
|
||||
const data = new Map<string, string>();
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: vi.fn((key: string) => data.get(key) ?? null),
|
||||
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
|
||||
removeItem: vi.fn((key: string) => data.delete(key)),
|
||||
});
|
||||
}
|
||||
|
||||
function mqttConnection(): ConnectionConfig {
|
||||
return {
|
||||
id: "mqtt-1",
|
||||
name: "test-mqtt",
|
||||
db_type: "mqtt",
|
||||
host: "127.0.0.1",
|
||||
port: 1883,
|
||||
user: "",
|
||||
password: "",
|
||||
database: "",
|
||||
readonly: false,
|
||||
read_only: false,
|
||||
ssl_mode: "disabled",
|
||||
color: "#888",
|
||||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
function mockApi() {
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listDatabases: vi.fn().mockResolvedValue([]),
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
}
|
||||
|
||||
describe("connectionStore MQTT sidebar tree", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllGlobals();
|
||||
installLocalStorage();
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("stores a stable i18n key for the MQTT console node", async () => {
|
||||
mockApi();
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = mqttConnection();
|
||||
const node: TreeNode = {
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
};
|
||||
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [node];
|
||||
|
||||
await store.refreshTreeNode(node);
|
||||
|
||||
expect(node.children).toHaveLength(1);
|
||||
expect(node.children?.[0]).toMatchObject({
|
||||
id: `${connection.id}:mqtt-topic:__console__`,
|
||||
type: "mqtt-topic",
|
||||
label: "connection.mqttConsoleTitle",
|
||||
});
|
||||
});
|
||||
|
||||
it("updates an existing MQTT tab title across locale changes and reopen", async () => {
|
||||
mockApi();
|
||||
|
||||
const { default: i18n, setLocale } = await import("@/i18n");
|
||||
const { tabDisplayTitle } = await import("@/lib/tabs/tabPresentation");
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
connectionStore.connections = [mqttConnection()];
|
||||
const translate = (key: string) => i18n.global.t(key);
|
||||
|
||||
await setLocale("en");
|
||||
const originalTabId = queryStore.openMqttAdmin("mqtt-1");
|
||||
const originalTab = queryStore.tabs.find((candidate) => candidate.id === originalTabId);
|
||||
|
||||
expect(originalTab?.title).toBe("connection.mqttConsoleTitle");
|
||||
expect(originalTab && tabDisplayTitle(originalTab, translate)).toBe("test-mqtt - MQTT Console");
|
||||
|
||||
await setLocale("zh-CN");
|
||||
const reusedTabId = queryStore.openMqttAdmin("mqtt-1");
|
||||
|
||||
expect(reusedTabId).toBe(originalTabId);
|
||||
expect(queryStore.tabs).toHaveLength(1);
|
||||
expect(originalTab && tabDisplayTitle(originalTab, translate)).toBe("test-mqtt - MQTT 控制台");
|
||||
|
||||
queryStore.closeTab(originalTabId, { force: true });
|
||||
const reopenedTabId = queryStore.openMqttAdmin("mqtt-1");
|
||||
const reopenedTab = queryStore.tabs.find((candidate) => candidate.id === reopenedTabId);
|
||||
|
||||
expect(reopenedTabId).not.toBe(originalTabId);
|
||||
expect(reopenedTab?.title).toBe("connection.mqttConsoleTitle");
|
||||
expect(reopenedTab && tabDisplayTitle(reopenedTab, translate)).toBe("test-mqtt - MQTT 控制台");
|
||||
});
|
||||
});
|
||||
@@ -3521,7 +3521,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
// exposes a single navigation entry and must not keep a second topic tree.
|
||||
const consoleNode: TreeNode = {
|
||||
id: `${connectionId}:mqtt-topic:__console__`,
|
||||
label: "MQTT 控制台",
|
||||
label: "connection.mqttConsoleTitle",
|
||||
type: "mqtt-topic" as const,
|
||||
connectionId,
|
||||
children: [],
|
||||
|
||||
@@ -1991,7 +1991,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const id = uuid();
|
||||
const tab: QueryTab = {
|
||||
id,
|
||||
title: `${conn?.name || "MQTT"} Console`,
|
||||
title: "connection.mqttConsoleTitle",
|
||||
connectionId,
|
||||
database: conn?.database || "",
|
||||
sql: "",
|
||||
|
||||
Reference in new issue
Block a user