From 18b6130c79cf59da36d16d8404e193ff6ee4f168 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Fri, 3 Oct 2025 09:42:23 +0200 Subject: [PATCH 1/7] absolute url impl --- src/config.test.ts | 59 ++++++++++++++++++++++++++++++++++++++++++++++ src/config.ts | 2 ++ 2 files changed, 61 insertions(+) create mode 100644 src/config.test.ts diff --git a/src/config.test.ts b/src/config.test.ts new file mode 100644 index 00000000000..279472faabc --- /dev/null +++ b/src/config.test.ts @@ -0,0 +1,59 @@ +import { getAbsoluteApiUrl, getApiUrl } from "@dashboard/config"; + +describe("global config", () => { + const { location } = window; + + beforeEach((): void => { + jest.clearAllMocks(); + + delete (window as { location?: unknown }).location; + + const testingUrl = new URL("https://foo.saleor.cloud/dashboard/product/asdf?aaaa=bbbb"); + + // Mock window.location for testing purposes + Object.defineProperty(window, "location", { + value: { + href: testingUrl.href, + hostname: testingUrl.hostname, + pathname: testingUrl.pathname, + }, + writable: true, + configurable: true, + }); + }); + afterAll((): void => { + Object.defineProperty(window, "location", { + value: location, + writable: true, + configurable: true, + }); + }); + + describe("getApiUrl", () => { + it.each(["/graphql/", "https://foo.saleor.cloud/graphql/"])( + "Returns value assigned to global window: %s", + param => { + window.__SALEOR_CONFIG__.API_URL = param; + + expect(getApiUrl()).toEqual(param); + }, + ); + }); + + describe("getAbsoluteApiUrl", () => { + it.each<{ envParam: string; expected: string }>([ + { + envParam: "/graphql/", + expected: "https://foo.saleor.cloud/graphql/", + }, + { + envParam: "https://foo.saleor.cloud/graphql/", + expected: "https://foo.saleor.cloud/graphql/", + }, + ])("Correctly builds absolute url: %s", ({ envParam, expected }) => { + window.__SALEOR_CONFIG__.API_URL = envParam; + + expect(getAbsoluteApiUrl()).toEqual(expected); + }); + }); +}); diff --git a/src/config.ts b/src/config.ts index bff60338291..4080c63fbb9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,7 +4,9 @@ import { ListSettings, ListViews, Pagination } from "./types"; export const getAppDefaultUri = () => "/"; export const getAppMountUri = () => window?.__SALEOR_CONFIG__?.APP_MOUNT_URI || getAppDefaultUri(); +// Can be `/graphql/` so don't rely on it export const getApiUrl = () => window.__SALEOR_CONFIG__.API_URL; +export const getAbsoluteApiUrl = () => new URL(getApiUrl(), window.location.origin).href; export const SW_INTERVAL = parseInt(process.env.SW_INTERVAL ?? "300", 10); export const IS_CLOUD_INSTANCE = window.__SALEOR_CONFIG__.IS_CLOUD_INSTANCE === "true"; From 26737ac8090b90a30cb2399885cc68244eb5f12e Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Fri, 3 Oct 2025 09:51:31 +0200 Subject: [PATCH 2/7] Fix absolute API URL resolution for extensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extensions were receiving incomplete URLs when the API_URL config was relative (e.g., /graphql/). This caused issues in setups where extensions needed to communicate back to the Saleor backend. Changes: - Introduced getAbsoluteApiUrl() to consistently resolve full API URLs - Replaced direct getApiUrl() calls with getAbsoluteApiUrl() in extension-related code - Added test coverage for absolute URL resolution - Simplified URL construction logic by centralizing absolute URL resolution 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .changeset/upset-apples-judge.md | 5 +++++ src/apps/components/AppWidgets/AppWidgets.tsx | 4 ++-- src/apps/urls.ts | 4 ++-- src/config.test.ts | 11 ++++++----- src/config.ts | 5 +++++ src/extensions/new-tab-actions.ts | 4 ++-- src/extensions/urls.ts | 4 ++-- 7 files changed, 24 insertions(+), 13 deletions(-) create mode 100644 .changeset/upset-apples-judge.md diff --git a/.changeset/upset-apples-judge.md b/.changeset/upset-apples-judge.md new file mode 100644 index 00000000000..dbb344c3089 --- /dev/null +++ b/.changeset/upset-apples-judge.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Fixed resolving Saleor absolute API URL. It was broken for some setups when extensions received imparital URL diff --git a/src/apps/components/AppWidgets/AppWidgets.tsx b/src/apps/components/AppWidgets/AppWidgets.tsx index d43a7212fb3..36b8d3e16a3 100644 --- a/src/apps/components/AppWidgets/AppWidgets.tsx +++ b/src/apps/components/AppWidgets/AppWidgets.tsx @@ -4,7 +4,7 @@ import { isUrlAbsolute } from "@dashboard/apps/isUrlAbsolute"; import { AppDetailsUrlMountQueryParams, AppUrls } from "@dashboard/apps/urls"; import { DashboardCard } from "@dashboard/components/Card"; import Link from "@dashboard/components/Link"; -import { APP_VERSION, getApiUrl } from "@dashboard/config"; +import { APP_VERSION, getAbsoluteApiUrl } from "@dashboard/config"; import { extensionActions } from "@dashboard/extensions/messages"; import { ExtensionWithParams } from "@dashboard/extensions/types"; import { AppExtensionTargetEnum } from "@dashboard/graphql"; @@ -83,7 +83,7 @@ const IframePost = ({ return (
- + <> diff --git a/src/apps/urls.ts b/src/apps/urls.ts index c0d12bdca8b..b0504adc904 100644 --- a/src/apps/urls.ts +++ b/src/apps/urls.ts @@ -1,4 +1,4 @@ -import { getApiUrl } from "@dashboard/config"; +import { getAbsoluteApiUrl } from "@dashboard/config"; import { FlagList } from "@dashboard/featureFlags"; import { stringifyQs } from "@dashboard/utils/urls"; import { ThemeType } from "@saleor/app-sdk/app-bridge"; @@ -77,7 +77,7 @@ export const AppUrls = { appUrl: string, params: AppDetailsUrlQueryParams & AppDetailsCommonParams, ) => { - const apiUrl = new URL(getApiUrl(), window.location.origin).href; + const apiUrl = getAbsoluteApiUrl(); /** * Use host to preserve port, in case of multiple Saleors running on localhost */ diff --git a/src/config.test.ts b/src/config.test.ts index 279472faabc..43d262c0f91 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -12,11 +12,8 @@ describe("global config", () => { // Mock window.location for testing purposes Object.defineProperty(window, "location", { - value: { - href: testingUrl.href, - hostname: testingUrl.hostname, - pathname: testingUrl.pathname, - }, + // URL matches fields from location so we can just put it there + value: testingUrl, writable: true, configurable: true, }); @@ -50,6 +47,10 @@ describe("global config", () => { envParam: "https://foo.saleor.cloud/graphql/", expected: "https://foo.saleor.cloud/graphql/", }, + { + envParam: "https://other.saleor.cloud/graphql/", + expected: "https://other.saleor.cloud/graphql/", + }, ])("Correctly builds absolute url: %s", ({ envParam, expected }) => { window.__SALEOR_CONFIG__.API_URL = envParam; diff --git a/src/config.ts b/src/config.ts index 4080c63fbb9..03f1434596d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,6 +6,11 @@ export const getAppDefaultUri = () => "/"; export const getAppMountUri = () => window?.__SALEOR_CONFIG__?.APP_MOUNT_URI || getAppDefaultUri(); // Can be `/graphql/` so don't rely on it export const getApiUrl = () => window.__SALEOR_CONFIG__.API_URL; +/** + * Resolves full API URL. + * If config provided with absolute url, will use it directly + * If config is relative, e.g. /graphql/ it will merge it with Dashboard URL + */ export const getAbsoluteApiUrl = () => new URL(getApiUrl(), window.location.origin).href; export const SW_INTERVAL = parseInt(process.env.SW_INTERVAL ?? "300", 10); export const IS_CLOUD_INSTANCE = window.__SALEOR_CONFIG__.IS_CLOUD_INSTANCE === "true"; diff --git a/src/extensions/new-tab-actions.ts b/src/extensions/new-tab-actions.ts index 072dc5ca7f4..220cc88f2b7 100644 --- a/src/extensions/new-tab-actions.ts +++ b/src/extensions/new-tab-actions.ts @@ -1,4 +1,4 @@ -import { getApiUrl } from "@dashboard/config"; +import { getAbsoluteApiUrl } from "@dashboard/config"; import { AppDetailsUrlMountQueryParams } from "@dashboard/extensions/urls"; const createInputElement = (name: string, value: string): HTMLInputElement => { @@ -57,7 +57,7 @@ export const newTabActions = { ...args.appParams, accessToken: args.accessToken, appId: args.appId, - saleorApiUrl: getApiUrl(), + saleorApiUrl: getAbsoluteApiUrl(), }; const form = document.createElement("form"); diff --git a/src/extensions/urls.ts b/src/extensions/urls.ts index 65272e38de3..d8ad7ef2c1a 100644 --- a/src/extensions/urls.ts +++ b/src/extensions/urls.ts @@ -1,5 +1,5 @@ import { AppPaths } from "@dashboard/apps/urls"; -import { getApiUrl } from "@dashboard/config"; +import { getAbsoluteApiUrl } from "@dashboard/config"; import { FlagList } from "@dashboard/featureFlags"; import { Dialog, SingleAction } from "@dashboard/types"; import { stringifyQs } from "@dashboard/utils/urls"; @@ -155,7 +155,7 @@ export const ExtensionsUrls = { appUrl: string, params: AppDetailsUrlQueryParams & AppDetailsCommonParams, ) => { - const apiUrl = new URL(getApiUrl(), window.location.origin).href; + const apiUrl = getAbsoluteApiUrl(); /** * Use host to preserve port, in case of multiple Saleors running on localhost */ From 816322633b052d7a89a6e5480f76587225fc8ac6 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Fri, 3 Oct 2025 10:03:57 +0200 Subject: [PATCH 3/7] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .changeset/upset-apples-judge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/upset-apples-judge.md b/.changeset/upset-apples-judge.md index dbb344c3089..a510781ddf6 100644 --- a/.changeset/upset-apples-judge.md +++ b/.changeset/upset-apples-judge.md @@ -2,4 +2,4 @@ "saleor-dashboard": patch --- -Fixed resolving Saleor absolute API URL. It was broken for some setups when extensions received imparital URL +Fixed resolving Saleor absolute API URL. It was broken for some setups when extensions received a partial (incomplete) URL. From 50cf3cede61aa2251dd26cdb5c67400731850dd0 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Fri, 3 Oct 2025 10:04:06 +0200 Subject: [PATCH 4/7] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 03f1434596d..90b7b113704 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,7 +4,7 @@ import { ListSettings, ListViews, Pagination } from "./types"; export const getAppDefaultUri = () => "/"; export const getAppMountUri = () => window?.__SALEOR_CONFIG__?.APP_MOUNT_URI || getAppDefaultUri(); -// Can be `/graphql/` so don't rely on it +// May be a relative path (e.g., '/graphql/'); use getAbsoluteApiUrl() when a fully qualified URL is required. export const getApiUrl = () => window.__SALEOR_CONFIG__.API_URL; /** * Resolves full API URL. From f2dd22257751d98050ede6dfca63663fd846acd5 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Fri, 3 Oct 2025 10:04:15 +0200 Subject: [PATCH 5/7] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 90b7b113704..9fc7134eec4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -8,7 +8,7 @@ export const getAppMountUri = () => window?.__SALEOR_CONFIG__?.APP_MOUNT_URI || export const getApiUrl = () => window.__SALEOR_CONFIG__.API_URL; /** * Resolves full API URL. - * If config provided with absolute url, will use it directly + * If the config provides an absolute URL, it will be used directly. * If config is relative, e.g. /graphql/ it will merge it with Dashboard URL */ export const getAbsoluteApiUrl = () => new URL(getApiUrl(), window.location.origin).href; From 3a4c7b77de3d89b314f56489e66b214d30187db6 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Fri, 3 Oct 2025 10:04:21 +0200 Subject: [PATCH 6/7] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 9fc7134eec4..375da721dcd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -9,7 +9,7 @@ export const getApiUrl = () => window.__SALEOR_CONFIG__.API_URL; /** * Resolves full API URL. * If the config provides an absolute URL, it will be used directly. - * If config is relative, e.g. /graphql/ it will merge it with Dashboard URL + * If the config is relative (e.g., /graphql/), it will be resolved against the Dashboard origin. */ export const getAbsoluteApiUrl = () => new URL(getApiUrl(), window.location.origin).href; export const SW_INTERVAL = parseInt(process.env.SW_INTERVAL ?? "300", 10); From c085e61c5d2cfa7bbd8bc7435035647688f3b66e Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Fri, 3 Oct 2025 10:10:09 +0200 Subject: [PATCH 7/7] fix tests --- src/apps/urls.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/urls.test.ts b/src/apps/urls.test.ts index 088874bf3c8..f3cc2f2312e 100644 --- a/src/apps/urls.test.ts +++ b/src/apps/urls.test.ts @@ -28,7 +28,7 @@ describe("AppUrls (apps/urls.ts)", () => { describe("For full URL provided in env", () => { beforeEach(() => { jest - .spyOn(config, "getApiUrl") + .spyOn(config, "getAbsoluteApiUrl") .mockImplementation(() => "https://shop.saleor.cloud/graphql/"); }); it.each<[string, string, Record & { theme: ThemeType }, string]>([