@gooddata/sdk-ui-pluggable-host 11.51.0-alpha.5 → 11.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"HostUiContainer.d.ts","sourceRoot":"","sources":["../../src/components/HostUiContainer.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EAA0B,KAAK,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AACpG,OAAO,EAEH,KAAK,gBAAgB,EAExB,MAAM,2CAA2C,CAAC;AAiBnD,OAAO,wBAAwB,CAAC;AAEhC,MAAM,WAAW,qBAAqB;IAClC,GAAG,EAAE,gBAAgB,CAAC;IACtB,IAAI,EAAE,gCAAgC,EAAE,CAAC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,gBAAgB,CAAC;CACpC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAE,EAAE,qBAAqB,2CA+R7F"}
1
+ {"version":3,"file":"HostUiContainer.d.ts","sourceRoot":"","sources":["../../src/components/HostUiContainer.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EAA0B,KAAK,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AACpG,OAAO,EAGH,KAAK,gBAAgB,EAExB,MAAM,2CAA2C,CAAC;AAiBnD,OAAO,wBAAwB,CAAC;AAGhC,MAAM,WAAW,qBAAqB;IAClC,GAAG,EAAE,gBAAgB,CAAC;IACtB,IAAI,EAAE,gCAAgC,EAAE,CAAC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,gBAAgB,CAAC;CACpC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAE,EAAE,qBAAqB,2CA+S7F"}
@@ -12,6 +12,7 @@ import { HostIntlProvider } from "../ui/HostIntlProvider.js";
12
12
  import { PluggableApplicationRenderer } from "../ui/PluggableApplicationRenderer.js";
13
13
  import { resolveHostUiModule } from "../ui/resolveHostUiModule.js";
14
14
  import "./HostUiContainer.scss";
15
+ import { runGuardedNavigation } from "./navigationGuard.js";
15
16
  /**
16
17
  * Mounts the host UI module into a container div, then renders the active
17
18
  * pluggable application into the host's app slot. Handles all lifecycle
@@ -63,6 +64,7 @@ export function HostUiContainer({ ctx, apps, pathname, routerNavigate }) {
63
64
  // app mount handle) and read by HostChat's chat so embedded apps can handle link clicks in-app.
64
65
  const appAiLinkClickRef = useRef(undefined);
65
66
  const onAppLinkClick = useCallback((link) => appAiLinkClickRef.current?.(link) ?? false, []);
67
+ const appNavigationRequestRef = useRef(undefined);
66
68
  const activeAppRef = useAutoupdateRef(activeInternalApplication);
67
69
  const onHeaderChange = useCallback((appId, header) => {
68
70
  if (activeAppRef.current?.id === appId) {
@@ -74,11 +76,19 @@ export function HostUiContainer({ ctx, apps, pathname, routerNavigate }) {
74
76
  setPageTitle(title);
75
77
  }
76
78
  }, [activeAppRef]);
77
- // Stable navigation callbacks that always use the latest router navigate
79
+ // Stable navigation callbacks that always use the latest router navigate.
80
+ // Both must stay stable: the host UI module captures them once at mount, so a changing dependency
81
+ // would leave it calling a stale closure.
78
82
  const navigateRef = useAutoupdateRef(routerNavigate);
83
+ // `replace` stays unguarded: it serves programmatic redirects, not user intent.
79
84
  const navigate = useCallback((url) => {
80
- void navigateRef.current(url);
81
- }, [navigateRef]);
85
+ runGuardedNavigation({
86
+ url,
87
+ currentPathname: latestMountStateRef.current.pathname,
88
+ guardRef: appNavigationRequestRef,
89
+ navigate: (target) => void navigateRef.current(target),
90
+ });
91
+ }, [navigateRef, latestMountStateRef]);
82
92
  const replace = useCallback((url) => {
83
93
  void navigateRef.current(url, { replace: true });
84
94
  }, [navigateRef]);
@@ -130,8 +140,12 @@ export function HostUiContainer({ ctx, apps, pathname, routerNavigate }) {
130
140
  appRootRef.current = undefined;
131
141
  const handle = handleRef.current;
132
142
  handleRef.current = undefined;
133
- appRoot?.unmount();
134
- handle?.unmount();
143
+ // This cleanup runs synchronously inside the commit of the root that renders this
144
+ // component, and unmount() is synchronous — doing it here re-enters React mid-render.
145
+ queueMicrotask(() => {
146
+ appRoot?.unmount();
147
+ handle?.unmount();
148
+ });
135
149
  };
136
150
  // Mount only once; updates are pushed via handle
137
151
  }, [latestMountStateRef, navigationMountRef]);
@@ -200,7 +214,7 @@ export function HostUiContainer({ ctx, apps, pathname, routerNavigate }) {
200
214
  return;
201
215
  }
202
216
  if (activeInternalApplication) {
203
- appRootRef.current.render(_jsx(HostIntlProvider, { locale: resolveLocale(ctx.preferredLocale), children: _jsx(PluggableApplicationRenderer, { app: activeInternalApplication, ctx: ctx, pathname: pathname, aiAssistantOpen: aiAssistantOpen, onOpenAiAssistant: requestOpenAi, onCloseAiAssistant: requestCloseAi, onAiAssistantContext: setAiAssistantContext, aiLinkClickHandlerRef: appAiLinkClickRef, onHeaderChange: onHeaderChange, onDocumentTitleChange: onDocumentTitleChange }, activeInternalApplication.id) }));
217
+ appRootRef.current.render(_jsx(HostIntlProvider, { locale: resolveLocale(ctx.preferredLocale), children: _jsx(PluggableApplicationRenderer, { app: activeInternalApplication, ctx: ctx, pathname: pathname, aiAssistantOpen: aiAssistantOpen, onOpenAiAssistant: requestOpenAi, onCloseAiAssistant: requestCloseAi, onAiAssistantContext: setAiAssistantContext, aiLinkClickHandlerRef: appAiLinkClickRef, navigationRequestRef: appNavigationRequestRef, onHeaderChange: onHeaderChange, onDocumentTitleChange: onDocumentTitleChange }, activeInternalApplication.id) }));
204
218
  }
205
219
  else {
206
220
  appRootRef.current.render(null);
@@ -0,0 +1,10 @@
1
+ import { type RefObject } from "react";
2
+ import { type IHostNavigationRequest } from "@gooddata/sdk-pluggable-application-model";
3
+ export declare function isSameDestination(targetUrl: string, currentPathname: string): boolean;
4
+ export declare function runGuardedNavigation(options: {
5
+ url: string;
6
+ currentPathname: string;
7
+ guardRef: RefObject<((request: IHostNavigationRequest) => boolean) | undefined>;
8
+ navigate: (url: string) => void;
9
+ }): void;
10
+ //# sourceMappingURL=navigationGuard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigationGuard.d.ts","sourceRoot":"","sources":["../../src/components/navigationGuard.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAE,KAAK,sBAAsB,EAAE,MAAM,2CAA2C,CAAC;AAKxF,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAKrF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;IAChF,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,GAAG,IAAI,CAmBP"}
@@ -0,0 +1,25 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { normalizePath, stripEmbedPrefix } from "../loader/routing.js";
3
+ // Pathname is the most the host can compare: it cannot see the module's in-app route.
4
+ export function isSameDestination(targetUrl, currentPathname) {
5
+ const targetPathname = targetUrl.split(/[?#]/)[0] ?? "";
6
+ return (normalizePath(stripEmbedPrefix(targetPathname)) === normalizePath(stripEmbedPrefix(currentPathname)));
7
+ }
8
+ export function runGuardedNavigation(options) {
9
+ const { url, currentPathname, guardRef, navigate } = options;
10
+ const guard = guardRef.current;
11
+ let consumed = false;
12
+ const proceed = () => {
13
+ // An application can retain `proceed`: a second call would push a duplicate history entry, and
14
+ // a call surviving into another mount would navigate on its behalf.
15
+ if (consumed || guardRef.current !== guard) {
16
+ return;
17
+ }
18
+ consumed = true;
19
+ navigate(url);
20
+ };
21
+ if (!isSameDestination(url, currentPathname) && guard?.({ url, proceed })) {
22
+ return;
23
+ }
24
+ proceed();
25
+ }
@@ -1,5 +1,6 @@
1
1
  import { type ApplicationScope, type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
2
2
  import { type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
3
+ export declare function stripEmbedPrefix(pathname: string): string;
3
4
  /**
4
5
  * Returns the application scope for the given URL path, or undefined if the path
5
6
  * does not match a known scope.
@@ -10,6 +11,7 @@ export declare function getApplicationScopeFromPath(path: string): ApplicationSc
10
11
  * does not contain a workspace id.
11
12
  */
12
13
  export declare function getWorkspaceIdFromPath(pathname: string | undefined): string | undefined;
14
+ export declare function normalizePath(path: string): string;
13
15
  /**
14
16
  * Substitutes `{workspaceId}` placeholders in an external app URL with the
15
17
  * currently resolved workspace id. URLs with no placeholder pass through
@@ -1 +1 @@
1
- {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/loader/routing.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,gBAAgB,EACrB,KAAK,gCAAgC,EAIxC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAQlF;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAYtF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAEvF;AA4CD;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAC7C,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,gBAAgB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAClB,MAAM,CAMR;AAED,wBAAgB,kBAAkB,CAC9B,GAAG,EAAE,gCAAgC,EACrC,GAAG,EAAE,gBAAgB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAClB,MAAM,CAWR;AAED,wBAAgB,wBAAwB,CACpC,GAAG,EAAE,gCAAgC,EACrC,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,MAAM,GACjB,OAAO,CAQT;AAED,wBAAgB,4BAA4B,CACxC,IAAI,EAAE,gCAAgC,EAAE,EACxC,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,MAAM,GACjB,gCAAgC,GAAG,SAAS,CAK9C"}
1
+ {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/loader/routing.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,gBAAgB,EACrB,KAAK,gCAAgC,EAIxC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAIlF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAYtF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAEvF;AAMD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMlD;AAgCD;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAC7C,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,gBAAgB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAClB,MAAM,CAMR;AAED,wBAAgB,kBAAkB,CAC9B,GAAG,EAAE,gCAAgC,EACrC,GAAG,EAAE,gBAAgB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAClB,MAAM,CAWR;AAED,wBAAgB,wBAAwB,CACpC,GAAG,EAAE,gCAAgC,EACrC,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,MAAM,GACjB,OAAO,CAQT;AAED,wBAAgB,4BAA4B,CACxC,IAAI,EAAE,gCAAgC,EAAE,EACxC,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,MAAM,GACjB,gCAAgC,GAAG,SAAS,CAK9C"}
@@ -1,7 +1,7 @@
1
1
  // (C) 2026 GoodData Corporation
2
2
  import { isExternalPluggableApplicationRegistryItem, isLocalPluggableApplicationRegistryItem, isRemotePluggableApplicationRegistryItem, } from "@gooddata/sdk-model";
3
3
  const WORKSPACE_PATH_PATTERN = /^\/workspace\/(?<workspaceId>[^/]+)(?:\/|$)/;
4
- function stripEmbedPrefix(pathname) {
4
+ export function stripEmbedPrefix(pathname) {
5
5
  return pathname.startsWith("/embedded/") ? pathname.slice("/embedded".length) : pathname;
6
6
  }
7
7
  /**
@@ -29,7 +29,7 @@ export function getWorkspaceIdFromPath(pathname) {
29
29
  function ensureLeadingSlash(path) {
30
30
  return path.startsWith("/") ? path : `/${path}`;
31
31
  }
32
- function normalizePath(path) {
32
+ export function normalizePath(path) {
33
33
  const prefixed = ensureLeadingSlash(path);
34
34
  if (prefixed === "/") {
35
35
  return prefixed;
@@ -1 +1 @@
1
- {"version":3,"file":"HostChrome.d.ts","sourceRoot":"","sources":["../../src/ui/HostChrome.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAuC,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAC;AAElG,OAAO,EAEH,KAAK,iBAAiB,EAEtB,KAAK,gCAAgC,EACxC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACxB,MAAM,2CAA2C,CAAC;AA+BnD,OAAO,mBAAmB,CAAC;AAK3B,OAAO,0CAA0C,CAAC;AAClD,OAAO,6CAA6C,CAAC;AACrD,OAAO,sDAAsD,CAAC;AAC9D,OAAO,0DAA0D,CAAC;AAYlE,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,gBAAgB,CAAC;IACtB,oBAAoB,EAAE,gCAAgC,EAAE,CAAC;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,YAAY,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC1C;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,2FAA2F;IAC3F,gBAAgB,CAAC,EAAE,CACf,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,iBAAiB,EAC/B,YAAY,CAAC,EAAE,OAAO,EACtB,kBAAkB,CAAC,EAAE,OAAO,KAC3B,IAAI,CAAC;IACV;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,wBAAgB,UAAU,CAAC,EACvB,GAAG,EACH,oBAAoB,EACpB,QAAQ,EACR,UAAU,EACV,SAAS,EAAE,UAAU,EACrB,aAAa,EACb,YAAmB,EACnB,YAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,QAAQ,EACX,EAAE,gBAAgB,2CA6NlB"}
1
+ {"version":3,"file":"HostChrome.d.ts","sourceRoot":"","sources":["../../src/ui/HostChrome.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAuC,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAC;AAElG,OAAO,EAEH,KAAK,iBAAiB,EAEtB,KAAK,gCAAgC,EACxC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACxB,MAAM,2CAA2C,CAAC;AA+BnD,OAAO,mBAAmB,CAAC;AAK3B,OAAO,0CAA0C,CAAC;AAClD,OAAO,6CAA6C,CAAC;AACrD,OAAO,sDAAsD,CAAC;AAC9D,OAAO,0DAA0D,CAAC;AAelE,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,gBAAgB,CAAC;IACtB,oBAAoB,EAAE,gCAAgC,EAAE,CAAC;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,YAAY,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC1C;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,2FAA2F;IAC3F,gBAAgB,CAAC,EAAE,CACf,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,iBAAiB,EAC/B,YAAY,CAAC,EAAE,OAAO,EACtB,kBAAkB,CAAC,EAAE,OAAO,KAC3B,IAAI,CAAC;IACV;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,wBAAgB,UAAU,CAAC,EACvB,GAAG,EACH,oBAAoB,EACpB,QAAQ,EACR,UAAU,EACV,SAAS,EAAE,UAAU,EACrB,aAAa,EACb,YAAmB,EACnB,YAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,QAAQ,EACX,EAAE,gBAAgB,2CA0OlB"}
@@ -11,7 +11,7 @@ import { getAppLifecycleCallbacks, preloadPluggableApplication, } from "../loade
11
11
  import { getActiveInternalApplication, getApplicationHref } from "../loader/routing.js";
12
12
  import { getBackend } from "../platformContext/backend.js";
13
13
  import { buildAppMenu, getLocalizedTitle } from "./appMenuItems.js";
14
- import { getUserDisplayName, swapWorkspaceInPath } from "./chromeHelpers.js";
14
+ import { getUserDisplayName, isPlainLeftClick, swapWorkspaceInPath } from "./chromeHelpers.js";
15
15
  import { b, e } from "./hostChromeBem.js";
16
16
  import { HostIntlProvider } from "./HostIntlProvider.js";
17
17
  import { HostNotificationDispatcher } from "./HostNotificationDispatcher.js";
@@ -35,6 +35,8 @@ const LOGOUT_MENU_ITEM_KEY = "gs.header.logout";
35
35
  // favicon is configured (see faviconUrl below).
36
36
  const initialFaviconUrl = (typeof document !== "undefined" && document.querySelector("link[rel~='icon']")?.getAttribute("href")) ||
37
37
  "/favicon.ico";
38
+ // Switches the host scope to organization; the first organization app is then chosen by redirect logic.
39
+ const LOGO_HREF = "/organization";
38
40
  export function HostChrome({ ctx, resolvedApplications, pathname, onNavigate, onReplace: _onReplace, headerOptions, notification = null, showChatItem = false, onChatToggle, onAskAiAssistant, appPageTitle, children, }) {
39
41
  const locale = resolveLocale(ctx.preferredLocale);
40
42
  const userName = getUserDisplayName(ctx.user);
@@ -100,6 +102,14 @@ export function HostChrome({ ctx, resolvedApplications, pathname, onNavigate, on
100
102
  }
101
103
  }
102
104
  }, [resolvedApplications, ctx, pathname]);
105
+ // Without this the anchor would do a full page load instead of a client-side navigation.
106
+ const handleLogoClick = useCallback((e) => {
107
+ if (!isPlainLeftClick(e)) {
108
+ return;
109
+ }
110
+ e.preventDefault();
111
+ onNavigate(LOGO_HREF);
112
+ }, [onNavigate]);
103
113
  const handleWorkspaceSelect = useCallback((workspace) => {
104
114
  const newId = workspace.id;
105
115
  if (!newId) {
@@ -143,7 +153,6 @@ export function HostChrome({ ctx, resolvedApplications, pathname, onNavigate, on
143
153
  const activeColor = ctx.theme?.header?.activeColor ?? defaultHeaderTheme.activeColor;
144
154
  const hideChrome = ctx.embeddingMode === "iframe" || ctx.isExportMode === true;
145
155
  return (_jsx(HostIntlProvider, { locale: locale, additionalMessages: appMessages, children: _jsx(BackendProvider, { backend: getBackend(), children: _jsx(ToastsCenterContextProvider, { children: _jsxs("div", { className: b(), children: [
146
- _jsx(DocumentHeader, { pageTitle: documentPageTitle, brandTitle: documentBrandTitle, faviconUrl: faviconUrl, appleTouchIconUrl: ctx.whiteLabeling?.appleTouchIconUrl }), hideChrome ? null : (_jsx("div", { className: e("header"), onMouseOver: handleHeaderMouseOver, children: _jsx(AppHeader, { logoUrl: ctx.whiteLabeling?.logoUrl || defaultLogoUrl, logoHref: "/organization" // switch the host scope to organization, the first org app will be chosen
147
- , logoTitle: logoTitle, headerColor: headerColor, headerTextColor: headerTextColor, activeColor: activeColor, userName: userName, organizationName: ctx.organization?.title, isAccessibilityCompliant: true, workspacePicker: workspacePicker, menuItemsGroups: menuItemsGroups, helpMenuItems: helpMenuItems, accountMenuItems: accountMenuItems, onMenuItemClick: handleMenuItemClick, showUpsellButton: pricing.isTrial, onUpsellButtonClick: pricing.onUpsellButtonClick, expiredDate: pricing.isTrial ? pricing.expiredDate : undefined, search: search.element, showChatItem: showChatItem, onChatItemClick: onChatToggle, notificationsPanel: ({ isMobile, closeNotificationsOverlay }) => (_jsx(AppHeaderNotifications, { locale: locale, isMobile: isMobile, closeNotificationsOverlay: closeNotificationsOverlay, useAsOfDateParam: true, enableExportToDocumentStorage: ctx.userSettings.enableExportToDocumentStorage ?? false })) }) })), _jsx("main", { className: e("content"), children: children }), pricing.element, _jsx(HostNotificationDispatcher, { notification: notification })
156
+ _jsx(DocumentHeader, { pageTitle: documentPageTitle, brandTitle: documentBrandTitle, faviconUrl: faviconUrl, appleTouchIconUrl: ctx.whiteLabeling?.appleTouchIconUrl }), hideChrome ? null : (_jsx("div", { className: e("header"), onMouseOver: handleHeaderMouseOver, children: _jsx(AppHeader, { logoUrl: ctx.whiteLabeling?.logoUrl || defaultLogoUrl, logoHref: LOGO_HREF, onLogoClick: handleLogoClick, logoTitle: logoTitle, headerColor: headerColor, headerTextColor: headerTextColor, activeColor: activeColor, userName: userName, organizationName: ctx.organization?.title, isAccessibilityCompliant: true, workspacePicker: workspacePicker, menuItemsGroups: menuItemsGroups, helpMenuItems: helpMenuItems, accountMenuItems: accountMenuItems, onMenuItemClick: handleMenuItemClick, showUpsellButton: pricing.isTrial, onUpsellButtonClick: pricing.onUpsellButtonClick, expiredDate: pricing.isTrial ? pricing.expiredDate : undefined, search: search.element, showChatItem: showChatItem, onChatItemClick: onChatToggle, notificationsPanel: ({ isMobile, closeNotificationsOverlay }) => (_jsx(AppHeaderNotifications, { locale: locale, isMobile: isMobile, closeNotificationsOverlay: closeNotificationsOverlay, useAsOfDateParam: true, enableExportToDocumentStorage: ctx.userSettings.enableExportToDocumentStorage ?? false })) }) })), _jsx("main", { className: e("content"), children: children }), pricing.element, _jsx(HostNotificationDispatcher, { notification: notification })
148
157
  ] }) }) }) }));
149
158
  }
@@ -1,6 +1,6 @@
1
1
  import { type RefObject } from "react";
2
2
  import { type IGenAIUserContext, type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
3
- import { type IAppHeaderOptions, type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
3
+ import { type IAppHeaderOptions, type IHostNavigationRequest, type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
4
4
  import "./PluggableApplicationRenderer.scss";
5
5
  export interface IPluggableApplicationRendererProps {
6
6
  app: PluggableApplicationRegistryItem;
@@ -30,8 +30,14 @@ export interface IPluggableApplicationRendererProps {
30
30
  itemUrl?: string;
31
31
  newTab?: boolean;
32
32
  }) => boolean) | undefined>;
33
+ /**
34
+ * Ref the renderer populates with a handler that asks the active app's mount handle
35
+ * (`onHostNavigationRequested`) whether the host may navigate away. Must be referentially
36
+ * stable — a new ref object remounts the application.
37
+ */
38
+ navigationRequestRef?: RefObject<((request: IHostNavigationRequest) => boolean) | undefined>;
33
39
  onHeaderChange?: (appId: string, header: IAppHeaderOptions) => void;
34
40
  onDocumentTitleChange?: (appId: string, pageTitle: string | undefined) => void;
35
41
  }
36
- export declare function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOpen, onOpenAiAssistant, onCloseAiAssistant, onAiAssistantContext, aiLinkClickHandlerRef, onHeaderChange, onDocumentTitleChange }: IPluggableApplicationRendererProps): import("react/jsx-runtime").JSX.Element;
42
+ export declare function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOpen, onOpenAiAssistant, onCloseAiAssistant, onAiAssistantContext, aiLinkClickHandlerRef, navigationRequestRef, onHeaderChange, onDocumentTitleChange }: IPluggableApplicationRendererProps): import("react/jsx-runtime").JSX.Element;
37
43
  //# sourceMappingURL=PluggableApplicationRenderer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PluggableApplicationRenderer.d.ts","sourceRoot":"","sources":["../../src/ui/PluggableApplicationRenderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAqD,MAAM,OAAO,CAAC;AAI1F,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AACpG,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EASxB,MAAM,2CAA2C,CAAC;AAcnD,OAAO,qCAAqC,CAAC;AAkB7C,MAAM,WAAW,kCAAkC;IAC/C,GAAG,EAAE,gCAAgC,CAAC;IACtC,GAAG,EAAE,gBAAgB,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,CAChB,QAAQ,CAAC,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,iBAAiB,EAC/B,YAAY,CAAC,EAAE,OAAO,EACtB,kBAAkB,CAAC,EAAE,OAAO,KAC3B,IAAI,CAAC;IACV,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,oHAAoH;IACpH,oBAAoB,CAAC,EAAE,CAAC,OAAO,EAAE;QAC7B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QACvB,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,WAAW,CAAC,EAAE,iBAAiB,CAAC;KACnC,KAAK,IAAI,CAAC;IACX;;;OAGG;IACH,qBAAqB,CAAC,EAAE,SAAS,CAC7B,CAAC,CAAC,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,GAAG,SAAS,CACtG,CAAC;IACF,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACpE,qBAAqB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CAClF;AAED,wBAAgB,4BAA4B,CAAC,EACzC,GAAG,EACH,GAAG,EACH,QAAQ,EACR,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,cAAc,EACd,qBAAqB,EACxB,EAAE,kCAAkC,2CAiQpC"}
1
+ {"version":3,"file":"PluggableApplicationRenderer.d.ts","sourceRoot":"","sources":["../../src/ui/PluggableApplicationRenderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAqD,MAAM,OAAO,CAAC;AAI1F,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AACpG,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EASxB,MAAM,2CAA2C,CAAC;AAcnD,OAAO,qCAAqC,CAAC;AAkB7C,MAAM,WAAW,kCAAkC;IAC/C,GAAG,EAAE,gCAAgC,CAAC;IACtC,GAAG,EAAE,gBAAgB,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,CAChB,QAAQ,CAAC,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,iBAAiB,EAC/B,YAAY,CAAC,EAAE,OAAO,EACtB,kBAAkB,CAAC,EAAE,OAAO,KAC3B,IAAI,CAAC;IACV,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,oHAAoH;IACpH,oBAAoB,CAAC,EAAE,CAAC,OAAO,EAAE;QAC7B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QACvB,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,WAAW,CAAC,EAAE,iBAAiB,CAAC;KACnC,KAAK,IAAI,CAAC;IACX;;;OAGG;IACH,qBAAqB,CAAC,EAAE,SAAS,CAC7B,CAAC,CAAC,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,GAAG,SAAS,CACtG,CAAC;IACF;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;IAC7F,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACpE,qBAAqB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CAClF;AAED,wBAAgB,4BAA4B,CAAC,EACzC,GAAG,EACH,GAAG,EACH,QAAQ,EACR,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACxB,EAAE,kCAAkC,2CA8RpC"}
@@ -20,7 +20,7 @@ const SECURITY_FAILURE_MESSAGES = {
20
20
  "build-expired": defineMessage({ id: "gs.host.error.appBuildExpired" }),
21
21
  "metadata-missing": defineMessage({ id: "gs.host.error.appSecurityMetadataMissing" }),
22
22
  };
23
- export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOpen, onOpenAiAssistant, onCloseAiAssistant, onAiAssistantContext, aiLinkClickHandlerRef, onHeaderChange, onDocumentTitleChange, }) {
23
+ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOpen, onOpenAiAssistant, onCloseAiAssistant, onAiAssistantContext, aiLinkClickHandlerRef, navigationRequestRef, onHeaderChange, onDocumentTitleChange, }) {
24
24
  const intl = useIntl();
25
25
  const intlRef = useAutoupdateRef(intl);
26
26
  const ctxRef = useAutoupdateRef(ctx);
@@ -83,12 +83,14 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOp
83
83
  let cancelled = false;
84
84
  const mountId = `${app.id}:${Date.now()}`;
85
85
  const totalStart = now();
86
+ // Identity must change per mount — the host rejects a `proceed` retained from an earlier one.
87
+ const navigationRequest = (request) => mountHandleRef.current?.onHostNavigationRequested?.(request) ?? false;
86
88
  const prevHandle = mountHandleRef.current;
87
89
  mountHandleRef.current = undefined;
88
90
  mountedAppRef.current = undefined;
89
91
  setViewState({ state: "loading" });
90
- // Defer unmount of the previous app to avoid synchronously unmounting
91
- // a React root while React is already rendering (race in strict mode).
92
+ // unmount() is synchronous, so calling it inline can re-enter React while the parent render
93
+ // that swapped this application out is still in progress.
92
94
  if (prevHandle) {
93
95
  queueMicrotask(() => prevHandle.unmount());
94
96
  }
@@ -126,6 +128,9 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOp
126
128
  onHeaderChange: (header) => onHeaderChangeRef.current?.(app.id, header),
127
129
  });
128
130
  mountedAppRef.current = { app, loadedApp };
131
+ if (navigationRequestRef) {
132
+ navigationRequestRef.current = navigationRequest;
133
+ }
129
134
  lifecycle?.onMountCompleted?.(app.id, now() - mountStart);
130
135
  lifecycle?.onRendered?.(app.id, now() - totalStart);
131
136
  setViewState({ state: "ready", validUntil: getSecuredRemoteAppValidUntil(app, loadedApp) });
@@ -134,6 +139,11 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOp
134
139
  if (cancelled) {
135
140
  return;
136
141
  }
142
+ // An application shown as failed must not veto navigation — its dialog would render
143
+ // into a hidden container.
144
+ if (navigationRequestRef?.current === navigationRequest) {
145
+ navigationRequestRef.current = undefined;
146
+ }
137
147
  const errorMessage = mountError instanceof Error ? mountError.message : "Unknown module loading error.";
138
148
  console.error(`[host-runtime/renderer] Failed to mount app "${app.id}".`, mountError);
139
149
  lifecycle?.onLoadFailed?.(app.id, errorMessage);
@@ -144,6 +154,10 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOp
144
154
  });
145
155
  return () => {
146
156
  cancelled = true;
157
+ // Only ever clear this mount's own registration, never a newer mount's.
158
+ if (navigationRequestRef?.current === navigationRequest) {
159
+ navigationRequestRef.current = undefined;
160
+ }
147
161
  const handle = mountHandleRef.current;
148
162
  if (handle) {
149
163
  mountHandleRef.current = undefined;
@@ -154,7 +168,17 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOp
154
168
  });
155
169
  }
156
170
  };
157
- }, [app, appBasePath, ctxRef, intlRef, onHeaderChangeRef, onTelemetryEvent, lifecycle, onEvent]);
171
+ }, [
172
+ app,
173
+ appBasePath,
174
+ ctxRef,
175
+ intlRef,
176
+ onHeaderChangeRef,
177
+ onTelemetryEvent,
178
+ lifecycle,
179
+ onEvent,
180
+ navigationRequestRef,
181
+ ]);
158
182
  useEffect(() => {
159
183
  const mounted = mountedAppRef.current;
160
184
  const handle = mountHandleRef.current;
@@ -171,6 +195,9 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOp
171
195
  console.error(`[host-runtime/renderer] Unmounting app "${mounted.app.id}" after context change: ${securityFailure.kind}.`, securityFailure);
172
196
  mountHandleRef.current = undefined;
173
197
  mountedAppRef.current = undefined;
198
+ if (navigationRequestRef) {
199
+ navigationRequestRef.current = undefined;
200
+ }
174
201
  handle.unmount();
175
202
  lifecycle?.onUnmounted?.(mounted.app.id);
176
203
  lifecycle?.onLoadFailed?.(mounted.app.id, securityFailure.kind);
@@ -178,7 +205,7 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOp
178
205
  return;
179
206
  }
180
207
  handle.updateContext?.(ctx);
181
- }, [ctx, intlRef, lifecycle]);
208
+ }, [ctx, intlRef, lifecycle, navigationRequestRef]);
182
209
  // Forward the host-owned chat open-state to the mounted app once it is ready, and on every
183
210
  // change, so app-side assistant controls stay aligned with the real (host) state (LX-2544).
184
211
  useEffect(() => {
@@ -1 +1 @@
1
- {"version":3,"file":"appMenuItems.d.ts","sourceRoot":"","sources":["../../src/ui/appMenuItems.ts"],"names":[],"mappings":"AAIA,OAAO,EACH,KAAK,OAAO,EACZ,KAAK,gCAAgC,EAExC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAwB,KAAK,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AACxG,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAkC5D,wBAAgB,iBAAiB,CAC7B,GAAG,EAAE,gCAAgC,EACrC,MAAM,EAAE,OAAO,GAAG,SAAS,GAC5B,MAAM,CAQR;AAED,MAAM,WAAW,cAAc;IAC3B,eAAe,EAAE,eAAe,EAAE,EAAE,CAAC;IACrC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAeD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CACxB,IAAI,EAAE,gCAAgC,EAAE,EACxC,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,GAAG,SAAS,GAC5B,cAAc,CA4BhB"}
1
+ {"version":3,"file":"appMenuItems.d.ts","sourceRoot":"","sources":["../../src/ui/appMenuItems.ts"],"names":[],"mappings":"AAIA,OAAO,EACH,KAAK,OAAO,EACZ,KAAK,gCAAgC,EAExC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAwB,KAAK,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AACxG,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AA2B5D,wBAAgB,iBAAiB,CAC7B,GAAG,EAAE,gCAAgC,EACrC,MAAM,EAAE,OAAO,GAAG,SAAS,GAC5B,MAAM,CAQR;AAED,MAAM,WAAW,cAAc;IAC3B,eAAe,EAAE,eAAe,EAAE,EAAE,CAAC;IACrC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAeD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CACxB,IAAI,EAAE,gCAAgC,EAAE,EACxC,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,GAAG,SAAS,GAC5B,cAAc,CA4BhB"}
@@ -2,18 +2,12 @@
2
2
  import { isExternalPluggableApplicationRegistryItem, } from "@gooddata/sdk-model";
3
3
  import { DefaultApplicationId } from "@gooddata/sdk-pluggable-application-model";
4
4
  import { getApplicationHref, isInternalAppRouteActive } from "../loader/routing.js";
5
+ import { isPlainLeftClick } from "./chromeHelpers.js";
5
6
  function handleExternalAppClick(href) {
6
7
  return (event) => {
7
- // Let the browser handle modifier-click and non-primary buttons natively
8
- // (opens in a new tab/window). Only intercept a plain left-click so we can
9
- // suppress the parent menu's SPA navigation and do a full-page load instead —
10
- // external apps live outside the host SPA, so React Router can't reach them.
11
- if (event.defaultPrevented ||
12
- event.button !== 0 ||
13
- event.metaKey ||
14
- event.ctrlKey ||
15
- event.shiftKey ||
16
- event.altKey) {
8
+ // Suppresses the parent menu's SPA navigation in favour of a full page load: external
9
+ // apps live outside the host SPA, so React Router cannot reach them.
10
+ if (!isPlainLeftClick(event)) {
17
11
  return;
18
12
  }
19
13
  event.preventDefault();
@@ -1,3 +1,4 @@
1
+ import { type MouseEvent } from "react";
1
2
  import { type IUser } from "@gooddata/sdk-model";
2
3
  /**
3
4
  * Builds the display name shown for the current user in the host header.
@@ -15,4 +16,12 @@ export declare function getUserDisplayName(user: IUser): string;
15
16
  * an organization-scoped route).
16
17
  */
17
18
  export declare function swapWorkspaceInPath(pathname: string, newWorkspaceId: string): string;
19
+ /**
20
+ * Whether a click on a link may be taken over with `preventDefault`, rather than left to the
21
+ * browser's own handling of the href.
22
+ *
23
+ * @remarks
24
+ * Keyboard activation of a focused link reports button 0, so it counts as a plain left click.
25
+ */
26
+ export declare function isPlainLeftClick(event: MouseEvent<Element>): boolean;
18
27
  //# sourceMappingURL=chromeHelpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"chromeHelpers.d.ts","sourceRoot":"","sources":["../../src/ui/chromeHelpers.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAEjD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAStD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAGpF"}
1
+ {"version":3,"file":"chromeHelpers.d.ts","sourceRoot":"","sources":["../../src/ui/chromeHelpers.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,OAAO,CAAC;AAExC,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAEjD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAStD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAGpF;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,OAAO,CASpE"}
@@ -27,3 +27,18 @@ export function swapWorkspaceInPath(pathname, newWorkspaceId) {
27
27
  const replaced = pathname.replace(/^\/workspace\/[^/]+/, `/workspace/${newWorkspaceId}`);
28
28
  return replaced === pathname ? `/workspace/${newWorkspaceId}` : replaced;
29
29
  }
30
+ /**
31
+ * Whether a click on a link may be taken over with `preventDefault`, rather than left to the
32
+ * browser's own handling of the href.
33
+ *
34
+ * @remarks
35
+ * Keyboard activation of a focused link reports button 0, so it counts as a plain left click.
36
+ */
37
+ export function isPlainLeftClick(event) {
38
+ return (!event.defaultPrevented &&
39
+ event.button === 0 &&
40
+ !event.metaKey &&
41
+ !event.ctrlKey &&
42
+ !event.shiftKey &&
43
+ !event.altKey);
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gooddata/sdk-ui-pluggable-host",
3
- "version": "11.51.0-alpha.5",
3
+ "version": "11.51.0",
4
4
  "description": "GoodData SDK runtime for hosting pluggable applications — registry, loader, routing, platform context, default UI chrome",
5
5
  "license": "MIT",
6
6
  "author": "GoodData Corporation",
@@ -29,20 +29,20 @@
29
29
  "dependencies": {
30
30
  "@module-federation/runtime": "2.6.0",
31
31
  "lodash-es": "^4.17.23",
32
- "@gooddata/sdk-backend-spi": "11.51.0-alpha.5",
33
- "@gooddata/sdk-backend-base": "11.51.0-alpha.5",
34
- "@gooddata/sdk-embedding": "11.51.0-alpha.5",
35
- "@gooddata/sdk-backend-tiger": "11.51.0-alpha.5",
36
- "@gooddata/sdk-model": "11.51.0-alpha.5",
37
- "@gooddata/sdk-ui": "11.51.0-alpha.5",
38
- "@gooddata/sdk-pluggable-application-model": "11.51.0-alpha.5",
39
- "@gooddata/sdk-ui-application-header": "11.51.0-alpha.5",
40
- "@gooddata/sdk-ui-ext": "11.51.0-alpha.5",
41
- "@gooddata/sdk-ui-kit": "11.51.0-alpha.5",
42
- "@gooddata/sdk-ui-gen-ai": "11.51.0-alpha.5",
43
- "@gooddata/util": "11.51.0-alpha.5",
44
- "@gooddata/sdk-ui-semantic-search": "11.51.0-alpha.5",
45
- "@gooddata/sdk-ui-theme-provider": "11.51.0-alpha.5"
32
+ "@gooddata/sdk-backend-base": "11.51.0",
33
+ "@gooddata/sdk-embedding": "11.51.0",
34
+ "@gooddata/sdk-backend-spi": "11.51.0",
35
+ "@gooddata/sdk-backend-tiger": "11.51.0",
36
+ "@gooddata/sdk-model": "11.51.0",
37
+ "@gooddata/sdk-pluggable-application-model": "11.51.0",
38
+ "@gooddata/sdk-ui-application-header": "11.51.0",
39
+ "@gooddata/sdk-ui-ext": "11.51.0",
40
+ "@gooddata/sdk-ui-gen-ai": "11.51.0",
41
+ "@gooddata/sdk-ui": "11.51.0",
42
+ "@gooddata/sdk-ui-kit": "11.51.0",
43
+ "@gooddata/sdk-ui-theme-provider": "11.51.0",
44
+ "@gooddata/sdk-ui-semantic-search": "11.51.0",
45
+ "@gooddata/util": "11.51.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@microsoft/api-documenter": "^7.17.0",
@@ -85,8 +85,8 @@
85
85
  "vite": "8.0.16",
86
86
  "vitest": "4.1.8",
87
87
  "vitest-dom": "0.1.1",
88
- "@gooddata/eslint-config": "11.51.0-alpha.5",
89
- "@gooddata/oxlint-config": "11.51.0-alpha.5"
88
+ "@gooddata/eslint-config": "11.51.0",
89
+ "@gooddata/oxlint-config": "11.51.0"
90
90
  },
91
91
  "peerDependencies": {
92
92
  "react": ">=18.3.1",