diff --git a/.claude/docs/harmonia-ui.md b/.claude/docs/harmonia-ui.md index 944a019a225..203101f86bc 100644 --- a/.claude/docs/harmonia-ui.md +++ b/.claude/docs/harmonia-ui.md @@ -14,7 +14,7 @@ The runtime UI stack for generated applications: they render as a self-contained ### Dirigible-specific facts the skill cannot know -- **One shared shell runtime, never copied per project.** It lives once at `/services/web/application-core/shell/` (`components/resources/application-core/.../application-core/shell/`: `app.js`, `services/*`, `stores/*`, `components/layout/appShell.js`, base/page components, `css/app.css`, the Inbox/Documents/Reports/notfound views). The generated per-project shell and the platform shells (`resources-application`, `-admin`, `-personal`, `-partner`, `-monitoring`, `-database`, `-builder`, `-home`, the tenant picker in `security-oauth2`) load it by absolute URL; `template-application-ui-harmonia-java` only **generates** the model-specific files (`index.html`, `config.js`, `dashboardPage.js`, `_settings.html`, `_dashboard.html`, per-entity pages). Do NOT re-add per-project copies of the shared files. +- **One shared shell runtime, never copied per project.** It lives once at `/services/web/application-core/shell/` (`components/resources/application-core/.../application-core/shell/`: `app.js`, `services/*`, `stores/*`, `components/layout/appShell.js`, base/page components, `css/app.css`, the Inbox/Documents/Reports/notfound views). The generated per-project shell and the platform shells (`resources-application`, `-admin`, `-personal`, `-partner`, `-monitoring`, `-database`, `-builder`, `-home`, the tenant picker in `security-oauth2`) load it by absolute URL; `template-application-ui-harmonia-java` only **generates** the model-specific files (`index.html`, `config.js`, `dashboardPage.js`, `_settings.html`, `_dashboard.html`, per-entity pages). Do NOT re-add per-project copies of the shared files. **Because it is shared, its page contract is public across generated vintages (#7427):** a project's `index.html` is rewritten only by an explicit Generate, so a platform upgrade swaps this runtime under pages generated months earlier. The properties a page binds and the `x-ref` names the runtime reaches for must stay alive when renamed - `appShell.js` keeps `hiddenPanels.left` / `isOpen` / `sidebarPanel` / `overlay` from before #7400 as aliases - and `ShellRuntimeVintageIT` is the guard: it puts the PREVIOUS template's rendered page (a fixture, not a regeneration) in the registry over the generated one and drives it in the browser, because every other Harmonia IT regenerates the page and so never sees old page + new runtime. - **Theme = Harmonia's colour-scheme API, three modes.** Harmonia persists the selection under `codbex.harmonia.colorMode` (default `auto`, the OS scheme), applies the `.dark` class itself and keeps every same-origin frame and browser tab in sync itself - nothing in this repo seeds the key or listens to `storage` events, and nothing may toggle `.dark` by hand. The shared `$store.theme` (`shell/js/stores/theme.js`) exposes `mode` (`light|dark|auto`, a getter/setter over `Harmonia.get/setColorScheme`) and `icon`, and re-syncs through `Harmonia.addColorSchemeListener((scheme, mode) => ...)`; every toolbar carries the same control - an icon button that is an `x-h-menu-trigger.dropdown` opening three `x-h-menu-radio-item`s bound to `$store.theme.mode`. Pages that load no shell runtime (home, tenant picker) carry the same getter/setter locally. - **Toasts go through the public `$notifications` magic.** A magic lives on an Alpine component scope, so the root component that renders the `x-h-notification-overlay` hands it to the shared store once at init (`Alpine.store('notifications').attachToaster(this.$notifications)`); `announce()` records the bell entry first and raises the toast through `add({ template: 'toast', position, timeout, data })`. Never reach into Harmonia's private `_h_notifications` store. - **`format.js` (`window.HarmoniaFormat`) stays the date/number engine**, not `x-h-date-format`: it preserves Java `DecimalFormat`/`DateTimeFormatter` pattern semantics and is dependency-free for the BPM task-form iframe. `HarmoniaDateFormatIT` asserts the `x-h-date-picker-popup="HarmoniaFormat.pickerConfig()"` contract. `window.HarmoniaFormat`, `window.HarmoniaCalendar` and `window.HarmoniaBranding` are Dirigible globals that only share the word; Harmonia itself sets only `window.Harmonia`. diff --git a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/layout/appShell.js b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/layout/appShell.js index b9c61fc21f2..8a9045efbe3 100644 --- a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/layout/appShell.js +++ b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/layout/appShell.js @@ -37,6 +37,17 @@ document.addEventListener('alpine:init', () => { // A route template is being fetched - drives the toolbar's indefinite progress bar. routeLoading: false, + // THE PAGE CONTRACT IS PUBLIC ACROSS GENERATED VINTAGES. This file is served once, by absolute URL, + // to every generated shell (#6094) - and a project's index.html is rewritten only by an explicit + // Generate, so a platform upgrade swaps this runtime under pages generated months earlier. The + // names a page binds (properties in its Alpine expressions, the x-ref names reached for below) + // must therefore stay alive when they are renamed. A page generated before #7400 binds its split + // panel to `hiddenPanels.left`, its sheet to `isOpen` and carries `sidebarPanel` / `overlay` refs; + // dropping them left every deployed app's embedded iframe showing its own sidebar (#7427). + get hiddenPanels() { return { left: this.isSmallScreen || this.embedded }; }, + get isOpen() { return this.showSidebarSheet; }, + set isOpen(open) { this.showSidebarSheet = open; }, + currentPath: '', // Embedded mode: when this SPA is hosted inside the shared application shell (the platform @@ -72,8 +83,10 @@ document.addEventListener('alpine:init', () => { // The sidebar is ONE element, moved between its wide-screen slot and the drawer. Embedded // mode has no navigation of its own (the host shell owns it), so it parks in the drawer - // which is closed, and whose trigger is hidden - at every width. - const home = isNarrow || this.embedded ? this.$refs.sidebarSheet : this.$refs.sidebarSlot; - home.appendChild(this.$refs.sidebar); + // The refs by their current names, else by the names a page generated before #7400 carries. + const sheet = this.$refs.sidebarSheet || this.$refs.overlay; + const slot = this.$refs.sidebarSlot || this.$refs.sidebarPanel; + (isNarrow || this.embedded ? sheet : slot).appendChild(this.$refs.sidebar); }, 1024); }, diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/ShellRuntimeVintageIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/ShellRuntimeVintageIT.java new file mode 100644 index 00000000000..06350aab9c3 --- /dev/null +++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/ShellRuntimeVintageIT.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.integration.tests.ui.tests; + +import static com.codeborne.selenide.Condition.visible; + +import com.codeborne.selenide.Selenide; +import com.codeborne.selenide.WebDriverRunner; +import org.eclipse.dirigible.repository.api.IRepository; +import org.eclipse.dirigible.repository.api.IRepositoryStructure; +import org.eclipse.dirigible.tests.base.ProjectDeployer; +import org.eclipse.dirigible.tests.base.UserInterfaceIntegrationTest; +import org.eclipse.dirigible.tests.framework.browser.HtmlElementType; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.Dimension; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A generated shell page of an EARLIER vintage keeps working against the CURRENT shared runtime. + *

+ * The shell runtime ({@code application-core/shell/js/components/layout/appShell.js}) is served + * once, by absolute URL, to every generated {@code gen//index.html} - and that page is a + * file in the project, rewritten only by an explicit Generate. So a platform upgrade swaps the + * runtime under every page generated before it, and the names the page binds (its Alpine + * expressions, its {@code x-ref}s) are a contract across vintages. #7400 renamed them together with + * the template and every deployed application's embedded iframe showed its own sidebar (#7427); no + * test caught it because every other Harmonia IT REGENERATES the page and so only ever sees the + * current page with the current runtime. + *

+ * The fixture is the pre-#7400 template rendered by the real pipeline for the + * {@code DependsOnHarmoniaIT} model - not hand-written, so it binds exactly what a deployed page of + * that vintage binds. The project is generated with the current template, then that page is put in + * the registry over the generated one, the way a deployed project keeps its page through an + * upgrade. One method on purpose: the base class discards the application context after each. + */ +class ShellRuntimeVintageIT extends UserInterfaceIntegrationTest { + + private static final String PROJECT = "DependsOnHarmoniaIT"; + private static final String PAGE = "/services/web/" + PROJECT + "/gen/edm/index.html"; + private static final String OLD_PAGE_FIXTURE = "ShellRuntimeVintageIT/index.html"; + private static final By SIDEBAR = By.cssSelector("[data-slot=sidebar]"); + + @Autowired + private ProjectDeployer deployer; + + @Autowired + private IRepository repository; + + @Test + void anOlderPageRendersAgainstTheCurrentRuntime() throws IOException { + deployer.deployGeneratedFromModel(PROJECT, "edm.model"); + repository.createResource(IRepositoryStructure.PATH_REGISTRY_PUBLIC + "/" + PROJECT + "/gen/edm/index.html", oldPage()); + + // Log in on the page itself, then navigate with the plain opener: the login redirect keeps the + // query but drops the fragment, and the route is in the fragment. + ide.openPath(PAGE); + + // Hosted in the application shell: the page draws no navigation of its own, the host owns it. + browser.openPath(PAGE + "?embedded=true#/Orders"); + browser.assertElementExistsByTypeAndText(HtmlElementType.BUTTON, "New"); + Selenide.$(SIDEBAR) + .shouldNotBe(visible); + + // Standalone on a wide screen: the sidebar is the navigation. + browser.openPath(PAGE + "#/Orders"); + browser.assertElementExistsByTypeAndText(HtmlElementType.BUTTON, "New"); + Selenide.$(SIDEBAR) + .shouldBe(visible); + + // Standalone below the breakpoint: the sidebar lives in the drawer the hamburger opens. + WebDriverRunner.getWebDriver() + .manage() + .window() + .setSize(new Dimension(800, 900)); + Selenide.$(SIDEBAR) + .shouldNotBe(visible); + Selenide.$(By.cssSelector("button[aria-label=Navigation]")) + .shouldBe(visible) + .click(); + Selenide.$(SIDEBAR) + .shouldBe(visible); + } + + private static byte[] oldPage() throws IOException { + try (InputStream page = ShellRuntimeVintageIT.class.getClassLoader() + .getResourceAsStream(OLD_PAGE_FIXTURE)) { + if (page == null) { + throw new IllegalStateException("Missing fixture [" + OLD_PAGE_FIXTURE + "]"); + } + return page.readAllBytes(); + } + } +} diff --git a/tests/tests-integrations/src/main/resources/ShellRuntimeVintageIT/index.html b/tests/tests-integrations/src/main/resources/ShellRuntimeVintageIT/index.html new file mode 100644 index 00000000000..00fe23d07c6 --- /dev/null +++ b/tests/tests-integrations/src/main/resources/ShellRuntimeVintageIT/index.html @@ -0,0 +1,537 @@ + + + + + + + + + + + DependsOnHarmoniaIT + + + + + + + + + + + + +

+
+
+ + +
+ + +
+
+
+
+ + + DependsOnHarmoniaIT +
+
+
+ +
+
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+ +
+
+
+
    +
  • + +
  • +
+
+
+ +
+
+
+
    + +
+
+
+
+ +
+ +
+
+
+ + +
+
+ +
+ + +
+ + + +
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
+ + + +
+
+ +
+ +
+
+
    + +
+
+ + + +
    + +
  • +
    +
    +
    +
    +
    +
    +
  • +
    +
  • + +
  • +
+
+ +
+ +
+
+
+ +
+ + +
+
+
+

+ +
+
+ +
+
+
+ + +
+
+
+

+ +
+
+ +
+
+
+ + +
+
+
+

+ +
+
+

+
+
+ + +
+
+
+ + +
+
+
+
+

+ +
+
+
+
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

+ +
+
+ +
+
+
+ + + + +