Introduce a Module API to get application settings. (#34278)

* Add a module API to get application settings.

* fix test
This commit is contained in:
Will Hunt
2026-07-15 19:20:21 +00:00
committed by GitHub
parent 8e0ed84eac
commit cc76070639
9 changed files with 132 additions and 0 deletions
@@ -0,0 +1,34 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
import { getSampleFilePath } from "../../sample-files";
test.describe("Settings API", () => {
test.use({
displayName: "Manny",
config: {
modules: ["/modules/settings-module.js"],
setting_defaults: {
language: "de",
},
},
page: async ({ page }, use) => {
await page.route("/modules/settings-module.js", async (route) => {
await route.fulfill({ path: getSampleFilePath("settings-module.js") });
});
await use(page);
},
});
test("should read a config-resolved setting value via api.settings.getValue", async ({ page }) => {
const dialogPromise = page.waitForEvent("dialog");
await page.goto("/");
const dialog = await dialogPromise;
expect(dialog.message()).toBe("de");
});
});
@@ -0,0 +1,16 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
export default class SettingsModule {
static moduleApiVersion = "^*";
constructor(api) {
this.api = api;
}
async load() {
alert(this.api.settings.getValue("language"));
}
}
+2
View File
@@ -34,6 +34,7 @@ import { WidgetLifecycleApi } from "./WidgetLifecycleApi.ts";
import { WidgetApi } from "./WidgetApi.ts";
import { CustomisationsApi } from "./customisationsApi.ts";
import { ComposerApi } from "./ComposerApi.ts";
import { SettingsApi } from "./SettingsApi.ts";
import defaultDispatcher from "../dispatcher/dispatcher.ts";
const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) => {
@@ -98,6 +99,7 @@ export class ModuleApi implements Api {
public readonly client = new ClientApi();
public readonly stores = new StoresApi();
public readonly composer = new ComposerApi(defaultDispatcher);
public readonly settings = new SettingsApi();
public createRoot(element: Element): Root {
return createRoot(element);
+27
View File
@@ -0,0 +1,27 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { describe, it, expect, vi } from "vitest";
import SettingsStore from "../settings/SettingsStore";
import { SettingsApi } from "./SettingsApi";
describe("SettingsApi", () => {
it("should return the value from SettingsStore.getValue", () => {
const spy = vi.spyOn(SettingsStore, "getValue").mockReturnValue("en" as any);
const api = new SettingsApi();
expect(api.getValue("language")).toBe("en");
expect(spy).toHaveBeenCalledWith("language", undefined, undefined);
});
it("should pass roomId and excludeDefault through to SettingsStore.getValue", () => {
const spy = vi.spyOn(SettingsStore, "getValue").mockReturnValue(null);
const api = new SettingsApi();
api.getValue("m.setting", "!room:example.org", true);
expect(spy).toHaveBeenCalledWith("m.setting", "!room:example.org", true);
});
});
+16
View File
@@ -0,0 +1,16 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import type { SettingsApi as ISettingsApi } from "@element-hq/element-web-module-api";
import SettingsStore from "../settings/SettingsStore";
export class SettingsApi implements ISettingsApi {
public getValue<T = any>(settingName: string, roomId?: string | null, excludeDefault?: boolean): T | undefined {
//@ts-expect-error: SettingsStore.getValue will throw on an invalid setting name anyway.
return SettingsStore.getValue(settingName, roomId, excludeDefault);
}
}
@@ -61,6 +61,8 @@ export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiEx
readonly i18n: I18nApi;
readonly navigation: NavigationApi;
readonly rootNode: HTMLElement;
// @alpha
readonly settings: SettingsApi;
readonly stores: StoresApi;
// @alpha
readonly widget: WidgetApi;
@@ -478,6 +480,11 @@ export interface RoomViewProps {
// @alpha @deprecated (undocumented)
export type RuntimeModuleConstructor = new (api: ModuleApi) => RuntimeModule;
// @alpha
export interface SettingsApi {
getValue<T = any>(settingName: string, roomId?: string | null, excludeDefault?: boolean): T | undefined;
}
// @alpha
export interface SpacePanelItemProps {
className?: string;
+7
View File
@@ -24,6 +24,7 @@ import { type WidgetLifecycleApi } from "./widget-lifecycle.ts";
import { type WidgetApi } from "./widget.ts";
import { type CustomisationsApi } from "./customisations.ts";
import { type ComposerApi } from "./composer.ts";
import { type SettingsApi } from "./settings.ts";
/**
* Module interface for modules to implement.
@@ -166,6 +167,12 @@ export interface Api
*/
readonly composer: ComposerApi;
/**
* Allows modules to read application settings.
* @alpha Subject to change.
*/
readonly settings: SettingsApi;
/**
* Create a ReactDOM root for rendering React components.
* Exposed to allow modules to avoid needing to bundle their own ReactDOM.
+22
View File
@@ -0,0 +1,22 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/**
* API for reading application settings.
* @alpha Subject to change.
*/
export interface SettingsApi {
/**
* Gets the value of a setting, computed across all applicable levels
* (device, room, account, config, default, etc.).
* @param settingName - The name of the setting to read.
* @param roomId - Room ID to read a room-scoped value for, or null/undefined for a
* non-room-scoped value.
* @param excludeDefault - If true, do not fall back to the setting's default value.
*/
getValue<T = any>(settingName: string, roomId?: string | null, excludeDefault?: boolean): T | undefined;
}
+1
View File
@@ -35,5 +35,6 @@ export type * from "./api/widget-lifecycle";
export type * from "./api/widget";
export type * from "./api/customisations";
export { UIComponent } from "./api/customisations";
export type * from "./api/settings";
export * from "./api/watchable";
export type * from "./utils";