diff --git a/superset-frontend/packages/superset-ui-core/src/connection/normalizeBackendUrls.ts b/superset-frontend/packages/superset-ui-core/src/connection/normalizeBackendUrls.ts index 8375955d653..f717553ec59 100644 --- a/superset-frontend/packages/superset-ui-core/src/connection/normalizeBackendUrls.ts +++ b/superset-frontend/packages/superset-ui-core/src/connection/normalizeBackendUrls.ts @@ -159,27 +159,15 @@ export function normalizeBackendUrlString( return value; } -/** - * Recursively normalise URL fields in a JSON-shaped value. - * - * Returns a new value when normalisation changed anything; otherwise returns - * the input by reference so consumers can compare with `===`. - */ -export function normalizeBackendUrls(value: T, options: NormalizeOptions): T { - const root = stripTrailingSlash(options.applicationRoot); - if (!root) return value; - return walk(value, root) as T; -} - function walk(value: unknown, root: string): unknown { if (Array.isArray(value)) { let changed = false; - const out: unknown[] = new Array(value.length); + const out: unknown[] = []; for (let index = 0; index < value.length; index += 1) { const item = value[index]; const next = walk(item, root); if (next !== item) changed = true; - out[index] = next; + out.push(next); } return changed ? out : value; } @@ -190,10 +178,7 @@ function walk(value: unknown, root: string): unknown { for (const key of Object.keys(value)) { const fieldValue = value[key]; let nextValue: unknown; - if ( - NORMALIZED_URL_FIELDS.has(key) && - typeof fieldValue === 'string' - ) { + if (NORMALIZED_URL_FIELDS.has(key) && typeof fieldValue === 'string') { nextValue = normalizeBackendUrlString(fieldValue, { applicationRoot: root, }); @@ -208,3 +193,18 @@ function walk(value: unknown, root: string): unknown { return value; } + +/** + * Recursively normalise URL fields in a JSON-shaped value. + * + * Returns a new value when normalisation changed anything; otherwise returns + * the input by reference so consumers can compare with `===`. + */ +export function normalizeBackendUrls( + value: T, + options: NormalizeOptions, +): T { + const root = stripTrailingSlash(options.applicationRoot); + if (!root) return value; + return walk(value, root) as T; +} diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.subdirectory.test.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.subdirectory.test.tsx index e24356f1b65..9a82d5bad64 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.subdirectory.test.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.subdirectory.test.tsx @@ -44,10 +44,13 @@ import SliceHeaderControls, { SliceHeaderControlsProps } from '.'; // goes green. // ============================================================================= -const APPLICATION_ROOT_MOCK = jest.fn(() => ''); +// Variable name must start with `mock` so Jest's hoisted `jest.mock()` +// factory can reference it. Renaming this prefix breaks the suite with +// "module factory is not allowed to reference any out-of-scope variables". +const mockApplicationRoot = jest.fn(() => ''); jest.mock('src/utils/getBootstrapData', () => ({ - applicationRoot: () => APPLICATION_ROOT_MOCK(), + applicationRoot: () => mockApplicationRoot(), })); const SLICE_ID = 371; @@ -124,7 +127,7 @@ describe('SliceHeaderControls — Cmd-click "Edit chart" under subdirectory depl let openSpy: jest.SpyInstance; beforeEach(() => { - APPLICATION_ROOT_MOCK.mockReturnValue(''); + mockApplicationRoot.mockReturnValue(''); openSpy = jest.spyOn(window, 'open').mockImplementation(() => null); }); @@ -133,7 +136,7 @@ describe('SliceHeaderControls — Cmd-click "Edit chart" under subdirectory depl }); test('opens the unprefixed exploreUrl when application root is empty', async () => { - APPLICATION_ROOT_MOCK.mockReturnValue(''); + mockApplicationRoot.mockReturnValue(''); renderControls(); userEvent.click(screen.getByRole('button', { name: 'More Options' })); @@ -148,7 +151,7 @@ describe('SliceHeaderControls — Cmd-click "Edit chart" under subdirectory depl }); test('opens the prefixed exploreUrl when deployed under a subdirectory', async () => { - APPLICATION_ROOT_MOCK.mockReturnValue('/superset'); + mockApplicationRoot.mockReturnValue('/superset'); renderControls(); userEvent.click(screen.getByRole('button', { name: 'More Options' })); diff --git a/superset-frontend/src/utils/navigationUtils.ts b/superset-frontend/src/utils/navigationUtils.ts index 657686de6c5..4faa7f2709c 100644 --- a/superset-frontend/src/utils/navigationUtils.ts +++ b/superset-frontend/src/utils/navigationUtils.ts @@ -16,7 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { createElement, type AnchorHTMLAttributes, type ReactElement } from 'react'; +import { + createElement, + type AnchorHTMLAttributes, + type ReactElement, +} from 'react'; import { ensureAppRoot } from './pathUtils'; // =============================================================================