diff --git a/cypress/e2e/serviceNavigation.cy.ts b/cypress/e2e/serviceNavigation.cy.ts
index 0b5c6d89..9eb1df0c 100644
--- a/cypress/e2e/serviceNavigation.cy.ts
+++ b/cypress/e2e/serviceNavigation.cy.ts
@@ -4,24 +4,11 @@ describe("Service Navigation", () => {
describe("desktop", () => {
beforeEach(() => {
cy.resizeToDesktop();
- cy.visit("/index.html");
});
it("open/closes user settings", () => {
- // User settings menu initially closed
- cy.get("button[aria-label='Menü Benutzereinstellungen']")
- .as("toggle")
- .should("be.visible")
- .ariaExpanded(false);
- cy.get("bkd-user-settings")
- .find('ul[role="menu"]')
- .as("serviceNav")
- .should("not.be.visible");
-
- // Open user settings menu
- cy.get("@toggle").click();
- cy.get("@toggle").ariaExpanded(true);
- cy.get("@serviceNav").should("be.visible");
+ cy.visit("/index.html");
+ openServiceNavigation();
// Close user settings menu
cy.get("@toggle").click();
@@ -30,20 +17,10 @@ describe("Service Navigation", () => {
});
it("renders user settings in the service navigation", () => {
- // User settings menu initially closed
- cy.get("button[aria-label='Menü Benutzereinstellungen']")
- .as("toggle")
- .should("be.visible")
- .ariaExpanded(false);
- cy.get("bkd-user-settings")
- .find('ul[role="menu"]')
- .as("serviceNav")
- .should("not.be.visible");
+ cy.visit("/index.html");
+ openServiceNavigation();
- // Open user settings menu
- cy.get("@toggle").click();
cy.get("@serviceNav")
- .should("be.visible")
.find("a")
.should((links) =>
expect(
@@ -60,20 +37,8 @@ describe("Service Navigation", () => {
// Apparently the triggering of the 'keydown' event does not work
// when run headless
it.skip("closes user settings on escape", () => {
- // User settings menu initially closed
- cy.get("button[aria-label='Menü Benutzereinstellungen']")
- .as("toggle")
- .should("be.visible")
- .ariaExpanded(false);
- cy.get("bkd-user-settings")
- .find('ul[role="menu"]')
- .as("serviceNav")
- .should("not.be.visible");
-
- // Open user settings menu
- cy.get("@toggle").click();
- cy.get("@toggle").ariaExpanded(true);
- cy.get("@serviceNav").should("be.visible");
+ cy.visit("/index.html");
+ openServiceNavigation();
// Close user settings menu
cy.document().trigger("keydown", { key: "Escape" });
@@ -82,20 +47,8 @@ describe("Service Navigation", () => {
});
it("closes user settings on select", () => {
- // User settings menu initially closed
- cy.get("button[aria-label='Menü Benutzereinstellungen']")
- .as("toggle")
- .should("be.visible")
- .ariaExpanded(false);
- cy.get("bkd-user-settings")
- .find('ul[role="menu"]')
- .as("serviceNav")
- .should("not.be.visible");
-
- // Open user settings menu
- cy.get("@toggle").click();
- cy.get("@toggle").ariaExpanded(true);
- cy.get("@serviceNav").should("be.visible");
+ cy.visit("/index.html");
+ openServiceNavigation();
// Close user settings menu
cy.get("@serviceNav")
@@ -107,20 +60,8 @@ describe("Service Navigation", () => {
});
it("closes user settings on click away", () => {
- // User settings menu initially closed
- cy.get("button[aria-label='Menü Benutzereinstellungen']")
- .as("toggle")
- .should("be.visible")
- .ariaExpanded(false);
- cy.get("bkd-user-settings")
- .find('ul[role="menu"]')
- .as("serviceNav")
- .should("not.be.visible");
-
- // Open user settings menu
- cy.get("@toggle").click();
- cy.get("@toggle").ariaExpanded(true);
- cy.get("@serviceNav").should("be.visible");
+ cy.visit("/index.html");
+ openServiceNavigation();
// Close user settings menu
cy.get("a.logo").click();
@@ -128,7 +69,10 @@ describe("Service Navigation", () => {
cy.get("@serviceNav").should("not.be.visible");
});
- it("renders language switcher in the service navigation", () => {
+ it("renders language switcher in the service navigation for multilingual schools", () => {
+ interceptGuiLanguagesRequest(["de-CH", "fr-CH"]);
+
+ cy.visit("/index.html");
cy.get("bkd-language-switcher")
.find("a")
.should((links) =>
@@ -137,28 +81,24 @@ describe("Service Navigation", () => {
).to.deep.eq(["de", "fr"]),
);
});
+
+ it("does not render language switcher in the service navigation for monolingual schools", () => {
+ interceptGuiLanguagesRequest(["de-CH"]);
+
+ cy.visit("/index.html");
+ cy.get("bkd-language-switcher").should("not.exist");
+ });
});
describe("mobile", () => {
beforeEach(() => {
cy.resizeToMobile();
- cy.visit("/index.html");
});
- it("renders user settings and language switcher in the mobile navigation", () => {
- // Mobile navigation initially closed
- cy.get("button[aria-label='Menü Benutzereinstellungen']")
- .should("not.be.visible")
- .ariaExpanded(false);
- cy.get("bkd-user-settings")
- .find('ul[role="menu"]')
- .should("not.be.visible");
-
- // Open mobile navigation
- cy.get("button[aria-label='Menü']").as("toggle");
- cy.get("@toggle").click();
- cy.get("@toggle").ariaExpanded(true);
- cy.get(".service-nav").as("serviceNav").should("be.visible");
+ it("renders user settings and language switcher in the mobile navigation for multilingual schools", () => {
+ interceptGuiLanguagesRequest(["de-CH", "fr-CH"]);
+ cy.visit("/index.html");
+ openMobileNavigation();
cy.get("@serviceNav")
.find("a")
@@ -175,5 +115,63 @@ describe("Service Navigation", () => {
]),
);
});
+
+ it("renders user settings in the mobile navigation for monolingual schools", () => {
+ interceptGuiLanguagesRequest(["de-CH"]);
+ cy.visit("/index.html");
+ openMobileNavigation();
+
+ cy.get("@serviceNav")
+ .find("a")
+ .should((links) =>
+ expect(
+ links.toArray().map((link) => link.textContent?.trim()),
+ ).to.deep.eq([
+ "Mein Profil",
+ "Einstellungen",
+ "Video-Tutorials",
+ "Logout",
+ ]),
+ );
+ });
});
});
+
+function interceptGuiLanguagesRequest(guiLanguage: string[]) {
+ cy.intercept(
+ "GET",
+ "https://eventotest.api/restApi/Configurations/SchoolAppNavigation",
+ { instanceName: "Test", guiLanguage },
+ );
+}
+
+function openServiceNavigation() {
+ // User settings menu initially closed
+ cy.get("button[aria-label='Menü Benutzereinstellungen']")
+ .as("toggle")
+ .should("be.visible")
+ .ariaExpanded(false);
+ cy.get("bkd-user-settings")
+ .find('ul[role="menu"]')
+ .as("serviceNav")
+ .should("not.be.visible");
+
+ // Open user settings menu
+ cy.get("@toggle").click();
+ cy.get("@toggle").ariaExpanded(true);
+ cy.get("@serviceNav").should("be.visible");
+}
+
+function openMobileNavigation() {
+ // Mobile navigation initially closed
+ cy.get("button[aria-label='Menü Benutzereinstellungen']")
+ .should("not.be.visible")
+ .ariaExpanded(false);
+ cy.get("bkd-user-settings").find('ul[role="menu"]').should("not.be.visible");
+
+ // Open mobile navigation
+ cy.get("button[aria-label='Menü']").as("toggle");
+ cy.get("@toggle").click();
+ cy.get("@toggle").ariaExpanded(true);
+ cy.get(".service-nav").as("serviceNav").should("be.visible");
+}
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index 23d9db78..8a783554 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -77,7 +77,7 @@ Cypress.Commands.add(
cy.intercept(
"GET",
"https://eventotest.api/restApi/Configurations/SchoolAppNavigation",
- { instanceName: "Test" },
+ { instanceName: "Test", guiLanguage: ["de-CH", "fr-CH"] },
);
cy.intercept(
diff --git a/src/components/Header/MobileNav.ts b/src/components/Header/MobileNav.ts
index a7ca484c..13a812fc 100644
--- a/src/components/Header/MobileNav.ts
+++ b/src/components/Header/MobileNav.ts
@@ -3,6 +3,7 @@ import { customElement, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { map } from "lit/directives/map.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
+import { when } from "lit/directives/when.js";
import { localized, msg } from "@lit/localize";
import { StateController } from "@lit-app/state";
import arrowDownIcon from "../../assets/icons/arrow-down.svg?raw";
@@ -316,7 +317,10 @@ export class MobileNav extends LitElement {
this.renderSettingsItem.bind(this),
)}
-
+ ${when(
+ portalState.allowedLocales.length > 1,
+ () => html``,
+ )}
`;
diff --git a/src/components/Header/ServiceNav.ts b/src/components/Header/ServiceNav.ts
index d3ec66c8..9dee196b 100644
--- a/src/components/Header/ServiceNav.ts
+++ b/src/components/Header/ServiceNav.ts
@@ -1,6 +1,9 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
+import { when } from "lit/directives/when.js";
import { localized, msg } from "@lit/localize";
+import { StateController } from "@lit-app/state";
+import { portalState } from "../../state/portal-state.ts";
import { theme } from "../../utils/theme";
@customElement("bkd-service-nav")
@@ -54,13 +57,21 @@ export class ServiceNav extends LitElement {
`,
];
+ constructor() {
+ super();
+ new StateController(this, portalState);
+ }
+
render() {
return html`
`;
diff --git a/src/state/portal-state.ts b/src/state/portal-state.ts
index 182a9c03..a09e070a 100644
--- a/src/state/portal-state.ts
+++ b/src/state/portal-state.ts
@@ -1,4 +1,3 @@
-import { msg } from "@lit/localize";
import { State, property, query } from "@lit-app/state";
import {
App,
@@ -7,7 +6,7 @@ import {
NavigationItem,
settings,
} from "../settings";
-import { fetchInstanceName, fetchUserAccessInfo } from "../utils/fetch";
+import { fetchInstanceInfo, fetchUserAccessInfo } from "../utils/fetch";
import { getInitialLocale, getLocale, updateLocale } from "../utils/locale";
import { filterAllowed, getApp, getNavigationItem } from "../utils/navigation";
import { cleanupQueryParams, updateQueryParam } from "../utils/routing";
@@ -29,6 +28,9 @@ class PortalState extends State {
@property({ value: "" })
instanceName!: string;
+ @property({ value: [] })
+ allowedLocales!: ReadonlyArray;
+
@property({ value: [] })
navigation!: Navigation;
@@ -68,6 +70,8 @@ class PortalState extends State {
);
async init() {
+ await this.loadInstanceInfo();
+
// Update initially
await this.handleStateChange("locale", this.locale);
@@ -160,7 +164,6 @@ class PortalState extends State {
private async handleStateChange(key: string, value: any): Promise {
if (key === "locale") {
await this.updateLocale(value);
- await this.loadInstanceName();
}
if (key === "locale" || key === "navigationItemKey") {
@@ -180,9 +183,15 @@ class PortalState extends State {
}
private async updateLocale(locale: PortalState["locale"]): Promise {
- updateQueryParam(LOCALE_QUERY_PARAM, locale);
+ // Fall back to allowed language (i.e. for instances that only support one
+ // language)
+ this.locale = this.allowedLocales.includes(locale)
+ ? locale
+ : this.allowedLocales[0];
+
+ updateQueryParam(LOCALE_QUERY_PARAM, this.locale);
try {
- await updateLocale(locale);
+ await updateLocale(this.locale);
} catch (error) {
console.warn(
"Unable to fetch/update locale (this may happen when interrupted by a redirect):",
@@ -257,14 +266,13 @@ class PortalState extends State {
this.rolesAndPermissions = [...roles, ...permissions];
}
- private async loadInstanceName(): Promise {
+ private async loadInstanceInfo(): Promise {
if (!tokenState.authenticated) return;
try {
- const instanceName = await fetchInstanceName();
- this.instanceName = [msg("Evento"), instanceName]
- .filter(Boolean)
- .join(" | ");
+ const { instanceName, allowedLocales } = await fetchInstanceInfo();
+ this.allowedLocales = allowedLocales;
+ this.instanceName = ["Evento", instanceName].filter(Boolean).join(" | ");
} catch (error) {
console.warn(
"Unable to fetch/update instance name (this may happen when interrupted by a redirect):",
diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts
index 4ed30e73..e317841c 100644
--- a/src/utils/fetch.ts
+++ b/src/utils/fetch.ts
@@ -23,11 +23,18 @@ export async function fetchUserAccessInfo(): Promise {
};
}
-export async function fetchInstanceName(): Promise {
- const url = `${envSettings.apiServer}/Configurations/SchoolAppNavigation`;
- const result = await fetchApi<{ instanceName: string }>(url);
+type InstanceInfo = Readonly<{
+ instanceName: string;
+ allowedLocales: ReadonlyArray;
+}>;
- return result?.instanceName || null;
+export async function fetchInstanceInfo(): Promise {
+ const url = `${envSettings.apiServer}/Configurations/SchoolAppNavigation`;
+ const { instanceName, guiLanguage: allowedLocales } = await fetchApi<{
+ instanceName: string;
+ guiLanguage: ReadonlyArray;
+ }>(url);
+ return { instanceName, allowedLocales };
}
export type Substitution = Readonly<{