From 06796a0a9d545667c1365e052f551949b6bb7313 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 8 May 2025 12:06:05 +0100 Subject: [PATCH] Create module element-web-module-banner --- modules/banner/README.md | 45 ++++ modules/banner/element-web/package.json | 29 +++ modules/banner/element-web/src/Banner.tsx | 65 ++++++ modules/banner/element-web/src/Logo.tsx | 39 ++++ modules/banner/element-web/src/Menu.tsx | 207 ++++++++++++++++++ .../element-web/src/Univention/Menu.tsx | 50 +++++ .../src/Univention/SilentLogin.tsx | 41 ++++ .../element-web/src/Univention/navigation.ts | 86 ++++++++ modules/banner/element-web/src/config.ts | 93 ++++++++ modules/banner/element-web/src/index.tsx | 46 ++++ modules/banner/element-web/src/theme.ts | 43 ++++ .../banner/element-web/src/translations.json | 22 ++ .../banner/element-web/tests/banner.spec.ts | 153 +++++++++++++ .../banner/element-web/tests/fixture/app1.png | Bin 0 -> 1588 bytes .../banner/element-web/tests/fixture/app2.png | Bin 0 -> 13818 bytes .../banner/element-web/tests/fixture/logo.svg | 50 +++++ .../element-web/tests/fixture/navigation.json | 44 ++++ .../tests/fixture/silent/index.html | 14 ++ modules/banner/element-web/tsconfig.json | 8 + modules/banner/element-web/vite.config.ts | 45 ++++ modules/opendesk/element-web/package.json | 2 +- 21 files changed, 1081 insertions(+), 1 deletion(-) create mode 100644 modules/banner/README.md create mode 100644 modules/banner/element-web/package.json create mode 100644 modules/banner/element-web/src/Banner.tsx create mode 100644 modules/banner/element-web/src/Logo.tsx create mode 100644 modules/banner/element-web/src/Menu.tsx create mode 100644 modules/banner/element-web/src/Univention/Menu.tsx create mode 100644 modules/banner/element-web/src/Univention/SilentLogin.tsx create mode 100644 modules/banner/element-web/src/Univention/navigation.ts create mode 100644 modules/banner/element-web/src/config.ts create mode 100644 modules/banner/element-web/src/index.tsx create mode 100644 modules/banner/element-web/src/theme.ts create mode 100644 modules/banner/element-web/src/translations.json create mode 100644 modules/banner/element-web/tests/banner.spec.ts create mode 100644 modules/banner/element-web/tests/fixture/app1.png create mode 100644 modules/banner/element-web/tests/fixture/app2.png create mode 100644 modules/banner/element-web/tests/fixture/logo.svg create mode 100644 modules/banner/element-web/tests/fixture/navigation.json create mode 100644 modules/banner/element-web/tests/fixture/silent/index.html create mode 100644 modules/banner/element-web/tsconfig.json create mode 100644 modules/banner/element-web/vite.config.ts diff --git a/modules/banner/README.md b/modules/banner/README.md new file mode 100644 index 0000000000..c62890c417 --- /dev/null +++ b/modules/banner/README.md @@ -0,0 +1,45 @@ +# @element-hq/element-web-module-banner + +Banner module for Element Web. +Allows rendering a top bar with slide out left panel menu. + +Supports the following configuration options: + +| Key | Type | Description | +| ------------- | ------ | ------------------------------------------------------------ | +| logo_url | string | URL to the logo to render in the banner | +| logo_link_url | string | URL to send the user to when clicking the logo in the banner | +| menu | `Menu` | Data to render in the banner menu | + +The `Menu` type is fulfilled by the following discriminated union: + +### Univention menu + +| Key | Type | Description | +| ------- | ------------ | ------------------------------------------------------------------------------------------------------------------------ | +| type | "univention" | The type for this menu config | +| ics_url | string | URL to the UCS Intercom Service, https://docs.software-univention.de/intercom-service/latest/architecture.html#endpoints | + +### Static menu + +| Key | Type | Description | +| ---------- | ---------------- | ------------------------------------------------------------------------- | +| type | "static" | The type for this menu config | +| logo_url | string, optional | URL to the logo to render in the menu, defaults to banner logo if omitted | +| categories | `[]Category` | Categories to render in the menu | + +The `Category` type is fulfilled by the following interface: + +| Key | Type | Description | +| ----- | -------- | -------------------------------- | +| name | string | The name of this category | +| links | `[]Link` | Links to render in this category | + +The `Link` type is fulfilled by the following interface: + +| Key | Type | Description | +| -------- | ---------------- | --------------------------------------- | +| icon_uri | string | URL to the icon to render for this link | +| name | string | The name to render for this link | +| link_url | string | The URL to link to | +| target | string, optional | The `target` to use for this link | diff --git a/modules/banner/element-web/package.json b/modules/banner/element-web/package.json new file mode 100644 index 0000000000..85ac88a46b --- /dev/null +++ b/modules/banner/element-web/package.json @@ -0,0 +1,29 @@ +{ + "name": "@element-hq/element-web-module-banner", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "lib/index.js", + "scripts": { + "prepare": "vite build", + "lint:ts": "tsc --noEmit", + "test": "echo no tests yet" + }, + "devDependencies": { + "@element-hq/element-web-module-api": "^0.2.0", + "@types/node": "^22.10.7", + "@types/react": "^19", + "@vitejs/plugin-react": "^4.3.4", + "react": "^19", + "rollup-plugin-external-globals": "^0.13.0", + "typescript": "^5.7.3", + "vite": "^6.0.11", + "vite-plugin-node-polyfills": "^0.23.0" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.6", + "framer-motion": "^12.4.10", + "styled-components": "^6.1.18", + "zod": "^3.24.2" + } +} diff --git a/modules/banner/element-web/src/Banner.tsx b/modules/banner/element-web/src/Banner.tsx new file mode 100644 index 0000000000..4ad63827ab --- /dev/null +++ b/modules/banner/element-web/src/Banner.tsx @@ -0,0 +1,65 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { type FC } from "react"; +import styled, { ThemeProvider } from "styled-components"; +import { type Api } from "@element-hq/element-web-module-api"; + +import { type ModuleConfig } from "./config"; +import UniventionMenu from "./Univention/Menu"; +import Menu from "./Menu"; +import { theme } from "./theme"; +import Logo from "./Logo.tsx"; + +const Root = styled.nav` + background-color: ${({ theme }) => theme.compound.color.bgCanvasDefault}; + border-bottom: ${({ theme }) => theme.navbar.border}; + height: ${({ theme }) => theme.navbar.height}; + display: grid; + grid-template-columns: ${({ theme }) => `${theme.navbar.triggerWidth} auto`}; + gap: 24px; +`; + +const Main = styled.div` + display: flex; + align-items: center; + grid-column: 2; +`; + +interface Props { + api: Api; + logoUrl: string; + href: string; + menu: ModuleConfig["menu"]; +} + +const Banner: FC = ({ api, logoUrl, href, menu }) => { + let menuJsx; + switch (menu.type) { + case "static": { + menuJsx = ; + break; + } + case "univention": { + menuJsx = ; + break; + } + } + + return ( + + + {menuJsx} +
+ +
+
+
+ ); +}; + +export default Banner; diff --git a/modules/banner/element-web/src/Logo.tsx b/modules/banner/element-web/src/Logo.tsx new file mode 100644 index 0000000000..48840486ca --- /dev/null +++ b/modules/banner/element-web/src/Logo.tsx @@ -0,0 +1,39 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { type FC } from "react"; +import styled from "styled-components"; +import { type Api } from "@element-hq/element-web-module-api"; + +const Anchor = styled.a` + display: flex; +`; + +const Image = styled.img` + height: ${({ theme }) => theme.navbar.logoHeight}; + align-self: center; +`; + +interface Props { + api: Api; + src: string; + href?: string; +} + +const Logo: FC = ({ api, src, href }) => { + const img = {api.i18n.translate("Portal; + + if (!href) return img; + + return ( + + {img} + + ); +}; + +export default Logo; diff --git a/modules/banner/element-web/src/Menu.tsx b/modules/banner/element-web/src/Menu.tsx new file mode 100644 index 0000000000..9ca1763858 --- /dev/null +++ b/modules/banner/element-web/src/Menu.tsx @@ -0,0 +1,207 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { type FC, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import * as Dialog from "@radix-ui/react-dialog"; +import styled from "styled-components"; + +import { StaticConfig } from "./config"; +import { theme } from "./theme"; +import type { Api } from "@element-hq/element-web-module-api"; +import Logo from "./Logo.tsx"; + +const Sidebar = styled(motion.div)` + padding: 16px 12px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); + overflow: auto; + position: fixed; + left: 0; + top: 0; + height: 100%; + width: ${({ theme }): string => theme.menu.width}; + background: white; + border-top-right-radius: 16px; + border-bottom-right-radius: 16px; +`; + +const SidebarHeading = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +`; + +const Launcher = styled.button` + align-items: center; + border: none; + color: ${({ theme }) => theme.compound.color.textPrimary}; + cursor: pointer; + display: flex; + + &:hover, + &:focus, + &[data-expanded="true"] { + background-color: ${({ theme }): string => theme.color.accent}; + color: #ffffff; + } + + svg { + margin: auto; + } +`; + +const CloseButton = styled.button` + /* Reset button styles */ + appearance: none; + background: none; + border: none; + padding: 0; + margin: 0; + + height: 32px; + width: 32px; + cursor: pointer; + border-radius: 8px; + + &:hover, + &:focus { + background-color: rgba(238, 239, 242, 1); + } +`; + +const CategoryHeading = styled.h2` + font-weight: 700; + font-size: 12px; + color: #203257; + margin-top: 16px; + margin-bottom: 8px; +`; + +const LinkButton = styled.a` + font-size: 14px; + color: #000000; + font-weight: 500; + display: flex; + border-radius: 8px; + padding: 8px; + align-items: center; + + &:hover { + background-color: #eeeff2; + } +`; + +const LinkLogo = styled.img` + height: 24px; + width: 24px; + border-radius: 3px; + border: 1px solid #eeeff2; + margin-right: 8px; + background-color: #ffffff; +`; + +const Overlay = styled(motion.div)` + background-color: rgba(238, 239, 242, 0.5); + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; +`; + +interface Props { + api: Api; + config: StaticConfig; + fallbackLogoUrl: string; +} + +const WIDTH = parseInt(theme.menu.width.slice(0, -2), 10); + +const Category: FC<{ + data: StaticConfig["categories"][number]; +}> = ({ data }) => { + return ( +
+ {data.name} + {data.links.map((link) => ( + + {link.name} + + ))} +
+ ); +}; + +const Menu: FC = ({ api, config, fallbackLogoUrl }) => { + const [open, setOpen] = useState(false); + + return ( + + + + + + + + + + + {open && ( + + + + + + + + + + setOpen(false)} + > + + + + + + + {config.categories.map((category) => ( + + ))} + + + + )} + + + ); +}; + +export default Menu; diff --git a/modules/banner/element-web/src/Univention/Menu.tsx b/modules/banner/element-web/src/Univention/Menu.tsx new file mode 100644 index 0000000000..690b03e94c --- /dev/null +++ b/modules/banner/element-web/src/Univention/Menu.tsx @@ -0,0 +1,50 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { FC, useEffect, useState } from "react"; + +import type { Api } from "@element-hq/element-web-module-api"; +import { StaticConfig, UniventionConfig } from "../config"; +import StaticMenu from "../Menu"; +import { fetchNavigation } from "./navigation"; +import SilentLogin from "./SilentLogin"; + +interface Props { + api: Api; + config: UniventionConfig; + fallbackLogoUrl: string; +} + +const Menu: FC = ({ api, config, fallbackLogoUrl }) => { + const [loggedIn, setLoggedIn] = useState(false); + const [data, setData] = useState(); + const language = api.i18n.language.toLowerCase().startsWith("de") ? "de-DE" : "en"; + + useEffect(() => { + let discard = false; + + setData(null); + fetchNavigation(config.ics_url, language).then((data) => { + if (discard) return; + setData(data); + }); + + return (): void => { + discard = true; + }; + }, [config, language, loggedIn]); + + if (!loggedIn) { + return ; + } + if (data) { + return ; + } + return
; +}; + +export default Menu; diff --git a/modules/banner/element-web/src/Univention/SilentLogin.tsx b/modules/banner/element-web/src/Univention/SilentLogin.tsx new file mode 100644 index 0000000000..06814333c2 --- /dev/null +++ b/modules/banner/element-web/src/Univention/SilentLogin.tsx @@ -0,0 +1,41 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { FC, useEffect } from "react"; +import styled from "styled-components"; + +const HiddenIFrame = styled.iframe` + display: none; +`; + +interface Props { + icsUrl: string; + onLoggedIn(success: boolean): void; +} + +const SilentLogin: FC = ({ onLoggedIn, icsUrl }) => { + const url = new URL("silent", icsUrl); + + useEffect(() => { + const listener = (event: MessageEvent): void => { + if (event.origin === url.origin && typeof event.data === "object" && event.data["loggedIn"] === true) { + onLoggedIn(true); + } + }; + + window.addEventListener("message", listener); + + return (): void => { + window.removeEventListener("message", listener); + }; + }, [onLoggedIn, url.origin]); + + // TODO title? + return ; +}; + +export default SilentLogin; diff --git a/modules/banner/element-web/src/Univention/navigation.ts b/modules/banner/element-web/src/Univention/navigation.ts new file mode 100644 index 0000000000..8443d2951f --- /dev/null +++ b/modules/banner/element-web/src/Univention/navigation.ts @@ -0,0 +1,86 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { z } from "zod"; + +import { StaticConfig } from "../config"; + +/** + * Univention Central Navigation API + * https://docs.software-univention.de/nubus-kubernetes-customization/1.x/en/api/central-navigation.html + */ +export const UniventionCentralNavigation = z.object({ + categories: z.array( + z.object({ + identifier: z.string(), + display_name: z.string(), + entries: z.array( + z.object({ + /** + * A unique identifier for the navigation item. + */ + identifier: z.string(), + /** + * The URL to the icon in SVG format. + */ + icon_url: z.string(), + /** + * The label of the link. + */ + display_name: z.string(), + /** + * The destination URL of the link. + */ + link: z.string(), + /** + * The browsing context in which the browser opens the link. + * Corresponds to the `target` property of `` tags in HTML. + */ + target: z.string(), + /** + * It’s usually empty. + */ + keywords: z.object({}).optional(), + }), + ), + }), + ), +}); + +type UniventionCentralNavigation = z.infer; + +function navigationToConfig(navigation: UniventionCentralNavigation): StaticConfig { + return { + type: "static", + categories: navigation.categories.map((category) => ({ + name: category.display_name, + links: category.entries.map((entry) => ({ + icon_uri: entry.icon_url, + name: entry.display_name, + link_url: entry.link, + target: entry.target, + })), + })), + }; +} + +export async function fetchNavigation(icsUrl: string, language: string): Promise { + const url = new URL("navigation.json", icsUrl); + url.search = `?language=${language}`; + + const response = await fetch(url, { + credentials: "include", + }); + + if (!response.ok) { + throw new Error(`Failed to fetch navigation: ${response.status}`); + } + + const data = await response.json(); + const config = await UniventionCentralNavigation.parseAsync(data); + return navigationToConfig(config); +} diff --git a/modules/banner/element-web/src/config.ts b/modules/banner/element-web/src/config.ts new file mode 100644 index 0000000000..3e5966bbbc --- /dev/null +++ b/modules/banner/element-web/src/config.ts @@ -0,0 +1,93 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { z } from "zod"; + +const StaticConfig = z.object({ + type: z.literal("static"), + + /** + * Alternative logo URL to display in the popover menu. + * Will use the main logo url if omitted. + */ + logo_url: z.string().url().optional(), + + /** + * Categories of links to display in the menu. + */ + categories: z.array( + z.object({ + /** + * The category display name + */ + name: z.string(), + /** + * List of links to display in the category + */ + links: z.array( + z.object({ + /** + * The URL to the icon. + */ + icon_uri: z.string().url(), + /** + * The label of the link. + */ + name: z.string(), + /** + * The destination URL of the link. + */ + link_url: z.string().url(), + /** + * The browsing context in which the browser opens the link. + */ + target: z.string().optional(), + }), + ), + }), + ), +}); + +export type StaticConfig = z.infer; + +const UniventionConfig = z.object({ + type: z.literal("univention"), + + /** + * Base URL to an Intercom Service + * https://docs.software-univention.de/intercom-service/latest/architecture.html#endpoints + */ + ics_url: z.string().url(), +}); + +export type UniventionConfig = z.infer; + +export const ModuleConfig = z.object({ + /** + * The URL of the portal logo.svg file. + * @example `https://example.com/logo.svg` + */ + logo_url: z.string().url(), + + /** + * The URL of the portal. + * @example `https://example.com` + */ + logo_link_url: z.string().url(), + + menu: z.discriminatedUnion("type", [StaticConfig, UniventionConfig]), +}); + +export type ModuleConfig = z.infer; + +export const CONFIG_KEY = "io.element.element-web-modules.banner"; + +declare module "@element-hq/element-web-module-api" { + export interface Config { + [CONFIG_KEY]: ModuleConfig; + } +} diff --git a/modules/banner/element-web/src/index.tsx b/modules/banner/element-web/src/index.tsx new file mode 100644 index 0000000000..9eeb972671 --- /dev/null +++ b/modules/banner/element-web/src/index.tsx @@ -0,0 +1,46 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import type { Module, Api, ModuleFactory } from "@element-hq/element-web-module-api"; +import Translations from "./translations.json"; +import { ModuleConfig, CONFIG_KEY } from "./config"; +import Banner from "./Banner"; +import { name as ModuleName } from "../package.json"; + +class TopBarModule implements Module { + public static readonly moduleApiVersion = "^0.2.0"; + + private config?: ModuleConfig; + + public constructor(private api: Api) {} + + public async load(): Promise { + this.api.i18n.register(Translations); + + try { + this.config = ModuleConfig.parse(this.api.config.get(CONFIG_KEY)); + } catch (e) { + console.error("Failed to init module", e); + throw new Error(`Errors in module configuration for "${ModuleName}"`); + } + + const div = document.createElement("div"); + this.api.rootNode.before(div); + + const root = this.api.createRoot(div); + root.render( + , + ); + } +} + +export default TopBarModule satisfies ModuleFactory; diff --git a/modules/banner/element-web/src/theme.ts b/modules/banner/element-web/src/theme.ts new file mode 100644 index 0000000000..4eec071127 --- /dev/null +++ b/modules/banner/element-web/src/theme.ts @@ -0,0 +1,43 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { DefaultTheme } from "styled-components"; + +const bgCanvasDefault = "var(--cpd-color-bg-canvas-default)"; +const textActionAccent = "var(--cpd-color-text-action-accent)"; +const textPrimary = "var(--cpd-color-text-primary)"; +const iconOnSolidPrimary = "var(--cpd-color-icon-on-solid-primary)"; + +const bodyMdSemibold = "var(--cpd-font-body-md-semibold)"; + +export const theme: DefaultTheme = { + compound: { + color: { + bgCanvasDefault, + textActionAccent, + textPrimary, + iconOnSolidPrimary, + }, + font: { + bodyMdSemibold, + }, + }, + color: { + accent: "#571EFA", // primary/700 TODO + }, + navbar: { + border: "1px solid #D3D7DE", // TODO + boxShadow: "4px 4px 12px 0 rgba(118, 131, 156, 0.6)", + height: "60px", + triggerWidth: "68px", + logoHeight: "34px", + }, + menu: { + // TODO + width: "235px", + }, +}; diff --git a/modules/banner/element-web/src/translations.json b/modules/banner/element-web/src/translations.json new file mode 100644 index 0000000000..a177686956 --- /dev/null +++ b/modules/banner/element-web/src/translations.json @@ -0,0 +1,22 @@ +{ + "Portal logo": { + "en": "Portal logo", + "de": "Portal Logo" + }, + "Menu": { + "en": "Menu", + "de": "Menü" + }, + "Show menu": { + "en": "Show menu", + "de": "Menü anzeigen" + }, + "Close menu": { + "en": "Close menu", + "de": "Menü schließen" + }, + "Show portal": { + "en": "Show portal", + "de": "Portal anzeigen" + } +} diff --git a/modules/banner/element-web/tests/banner.spec.ts b/modules/banner/element-web/tests/banner.spec.ts new file mode 100644 index 0000000000..7637b688ad --- /dev/null +++ b/modules/banner/element-web/tests/banner.spec.ts @@ -0,0 +1,153 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { test, expect } from "../../../../playwright/element-web-test.ts"; +import { ModuleConfig } from "../src/config.ts"; + +test.describe("Banner", () => { + test.use({ + displayName: "Timmy", + page: async ({ context, page, moduleDir }, use) => { + for (const path of ["logo.svg", "app1.png", "app2.png"]) { + await context.route(`/${path}*`, async (route) => { + await route.fulfill({ path: `${moduleDir}/tests/fixture/${path}` }); + }); + } + await context.route("http://localhost:8080/ics/navigation.json*", async (route) => { + await route.fulfill({ + path: `${moduleDir}/tests/fixture/navigation.json`, + contentType: "application/json", + }); + }); + await context.route("http://localhost:8080/ics/silent", async (route) => { + await route.fulfill({ path: `${moduleDir}/tests/fixture/silent/index.html`, contentType: "text/html" }); + }); + + await page.goto("/"); + await use(page); + }, + }); + + test("should error if config is missing", { tag: ["@screenshot"] }, async ({ page }) => { + await expect(page.getByText("Your Element is misconfigured")).toBeVisible(); + // We don't take a screenshot as we don't want to assert Element's styling, only our own + }); + + const configs: ModuleConfig[] = [ + { + logo_url: "http://localhost:8080/logo.svg", + logo_link_url: "https://example.com/portal", + menu: { + type: "static", + categories: [ + { + name: "Applications", + links: [ + { + icon_uri: "http://localhost:8080/app1.png", + name: "App 1", + link_url: "https://example.com/app1", + target: "app1", + }, + { + icon_uri: "http://localhost:8080/app2.png", + name: "Riot", + link_url: "https://riot.im/app", + target: "riot-im", + }, + ], + }, + { + name: "Links", + links: [ + { + icon_uri: "http://localhost:8080/app1.png", + name: "Link", + link_url: "https://example.com/link1", + }, + ], + }, + ], + }, + }, + { + logo_url: "http://localhost:8080/logo.svg", + logo_link_url: "https://example.com/portal", + menu: { + type: "univention", + ics_url: "http://localhost:8080/ics/", + }, + }, + ]; + + for (const config of configs) { + const type = config.menu.type; + + test.describe(`${type} config`, () => { + test.use({ + config: { + "io.element.element-web-modules.banner": config, + }, + }); + + test("should render", { tag: ["@screenshot"] }, async ({ page, axe }) => { + await expect(page.getByRole("heading", { name: "Welcome to Element!" })).toBeVisible(); + await expect(page.getByLabel("Show portal")).toHaveAttribute("href", "https://example.com/portal"); + + const nav = page.locator("nav"); + if (type === "univention") { + await expect(nav).toMatchScreenshot(`${type}_nav_loading.png`); + // The stub silent html doesn't seem to work in Playwright so send the postMessage manually + await page.evaluate(() => { + window.postMessage({ + loggedIn: true, + }); + }); + } + + const trigger = page.getByLabel("Show menu"); + await expect(trigger).toBeVisible(); + + // Assert the banner looks as we expect + await expect(nav).toMatchAriaSnapshot(); + await expect(nav).toMatchScreenshot(`${type}_nav.png`); + + // Check hover styles + await trigger.hover(); + await expect(nav).toMatchScreenshot(`${type}_nav_hover.png`); + + await test.step("open menu", async () => { + await trigger.click(); + const app1 = page.getByText("App 1"); + await expect(app1).toHaveAttribute("href", "https://example.com/app1"); + await app1.hover(); + + // Assert the sidebar looks as we expect + const sidebar = page.getByRole("dialog"); + await expect(sidebar).toMatchAriaSnapshot(); + await expect(page).toMatchScreenshot(`${type}_menu.png`, { + // We exclude this as we don't want to assert Element's styling, only our own + css: ` + #matrixchat { + opacity: 0; + background: orchid; + } + `, + }); + }); + + await test.step("close menu", async () => { + await expect(axe).toHaveNoViolations(); + + // Assert it closes by clicking the overlay + await page.getByTestId("dialog-overlay").click(); + await expect(page.getByRole("dialog")).not.toBeVisible(); + }); + }); + }); + } +}); diff --git a/modules/banner/element-web/tests/fixture/app1.png b/modules/banner/element-web/tests/fixture/app1.png new file mode 100644 index 0000000000000000000000000000000000000000..2c34416b885b9728da50b3d275652cec6e9d6004 GIT binary patch literal 1588 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 T^B6||yQ} + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/banner/element-web/tests/fixture/navigation.json b/modules/banner/element-web/tests/fixture/navigation.json new file mode 100644 index 0000000000..79bac98d25 --- /dev/null +++ b/modules/banner/element-web/tests/fixture/navigation.json @@ -0,0 +1,44 @@ +{ + "categories": [ + { + "identifier": "category1", + "display_name": "Category 1", + "entries": [ + { + "identifier": "app1", + "display_name": "App 1", + "icon_url": "http://localhost:8080/app1.png", + "link": "https://example.com/app1", + "target": "_blank" + }, + { + "identifier": "app2", + "display_name": "App 2", + "icon_url": "http://localhost:8080/app2.png", + "link": "https://example.com/app2", + "target": "_blank" + } + ] + }, + { + "identifier": "category2", + "display_name": "Category 2", + "entries": [ + { + "identifier": "app3", + "display_name": "App 3", + "icon_url": "http://localhost:8080/app1.png", + "link": "https://example.com/app3", + "target": "_blank" + }, + { + "identifier": "app4", + "display_name": "App 4", + "icon_url": "http://localhost:8080/app2.png", + "link": "https://example.com/app4", + "target": "_blank" + } + ] + } + ] +} diff --git a/modules/banner/element-web/tests/fixture/silent/index.html b/modules/banner/element-web/tests/fixture/silent/index.html new file mode 100644 index 0000000000..ae80633f60 --- /dev/null +++ b/modules/banner/element-web/tests/fixture/silent/index.html @@ -0,0 +1,14 @@ + + + + + + Univention Silent Login + + + + diff --git a/modules/banner/element-web/tsconfig.json b/modules/banner/element-web/tsconfig.json new file mode 100644 index 0000000000..fbda6380e7 --- /dev/null +++ b/modules/banner/element-web/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "lib", + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/modules/banner/element-web/vite.config.ts b/modules/banner/element-web/vite.config.ts new file mode 100644 index 0000000000..48d989a42c --- /dev/null +++ b/modules/banner/element-web/vite.config.ts @@ -0,0 +1,45 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { nodePolyfills } from "vite-plugin-node-polyfills"; +import externalGlobals from "rollup-plugin-external-globals"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, "src/index.tsx"), + name: "element-web-module-banner", + fileName: "index", + formats: ["es"], + }, + outDir: "lib", + target: "esnext", + sourcemap: true, + rollupOptions: { + external: ["react"], + }, + }, + plugins: [ + react(), + nodePolyfills({ + include: ["events"], + }), + externalGlobals({ + // Reuse React from the host app + react: "window.React", + }), + ], + define: { + process: { env: {} }, + }, +}); diff --git a/modules/opendesk/element-web/package.json b/modules/opendesk/element-web/package.json index e74754c4ce..438d65dfa1 100644 --- a/modules/opendesk/element-web/package.json +++ b/modules/opendesk/element-web/package.json @@ -12,7 +12,7 @@ "test": "echo no tests yet" }, "devDependencies": { - "@element-hq/element-web-module-api": "^0.1.0", + "@element-hq/element-web-module-api": "^0.2.0", "@nordeck/element-web-guest-module": "^2.0.0", "@nordeck/element-web-opendesk-module": "^0.5.0", "@nordeck/element-web-widget-lifecycle-module": "^1.0.1",