From 27ea5de44a087dec9ba768aacacf8e5962d239f5 Mon Sep 17 00:00:00 2001 From: Gaston Laterza Date: Fri, 21 Aug 2026 00:03:57 -0300 Subject: [PATCH 1/3] fix(i18n-es): correct case/number collapse in the semantic-layer labels (#43311) Co-authored-by: Claude Opus 5 (1M context) --- .../translations/es/LC_MESSAGES/messages.po | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/superset/translations/es/LC_MESSAGES/messages.po b/superset/translations/es/LC_MESSAGES/messages.po index aa9c0431a623..fb7ccc153017 100644 --- a/superset/translations/es/LC_MESSAGES/messages.po +++ b/superset/translations/es/LC_MESSAGES/messages.po @@ -3804,9 +3804,8 @@ msgstr "Radio de agrupación" msgid "Code" msgstr "Código" -#, fuzzy msgid "Code Copied!" -msgstr "SQL copiado" +msgstr "¡Código copiado!" #, fuzzy msgid "Collapse" @@ -4301,9 +4300,8 @@ msgstr "Controles etiquetados " msgid "Copied to clipboard!" msgstr "Copiado al portapapeles" -#, fuzzy msgid "Copied!" -msgstr "SQL copiado" +msgstr "¡Copiado!" msgid "Copy" msgstr "Copiar" @@ -4903,9 +4901,8 @@ msgstr "Con puntos" msgid "Data" msgstr "Datos" -#, fuzzy msgid "Data Connections" -msgstr "Conexiones de la base de datos" +msgstr "Conexiones de datos" msgid "Data Export Options" msgstr "Opciones de exportación de datos" @@ -4919,13 +4916,11 @@ msgstr "El URI de datos no está permitido." msgid "Data Zoom" msgstr "«Zoom» de datos" -#, fuzzy msgid "Data connection" -msgstr "Conexiones de la base de datos" +msgstr "Conexión de datos" -#, fuzzy msgid "Data connections" -msgstr "Conexiones de la base de datos" +msgstr "Conexiones de datos" msgid "" "Data could not be deserialized from the results backend. The storage " @@ -5165,9 +5160,8 @@ msgstr "" "Se requiere el tipo de fuente de datos cuando se proporciona un ID de " "fuentes de datos" -#, fuzzy msgid "Datasources" -msgstr "Fuente de datos" +msgstr "Fuentes de datos" msgid "Date Time Format" msgstr "Formato de fecha y hora" @@ -9874,9 +9868,8 @@ msgstr "Error de red al intentar recuperar el recurso" msgid "Network error." msgstr "Error de red." -#, fuzzy msgid "New" -msgstr "Ahora" +msgstr "Nuevo" #, -ERR:PROP-NOT-FOUND- msgid "New Semantic Layer" @@ -19340,20 +19333,17 @@ msgstr "panel de control" msgid "dashboards" msgstr "Paneles de control" -#, fuzzy msgid "data connection" -msgstr "Conexiones de la base de datos" +msgstr "conexión de datos" -#, fuzzy msgid "data connections" -msgstr "Conexiones de la base de datos" +msgstr "conexiones de datos" msgid "database" msgstr "base de datos" -#, fuzzy msgid "databases" -msgstr "Bases de datos" +msgstr "bases de datos" msgid "dataset" msgstr "conjunto de datos" @@ -19361,17 +19351,14 @@ msgstr "conjunto de datos" msgid "dataset name" msgstr "nombre del conjunto de datos" -#, fuzzy msgid "datasets" -msgstr "Conjuntos de datos" +msgstr "conjuntos de datos" -#, fuzzy msgid "datasource" -msgstr "Fuente de datos" +msgstr "fuente de datos" -#, fuzzy msgid "datasources" -msgstr "Fuente de datos" +msgstr "fuentes de datos" msgid "date" msgstr "fecha" From 22396d504a5237b4ef8a5beb7edc1343e2722242 Mon Sep 17 00:00:00 2001 From: Kasia <36897697+kasiazjc@users.noreply.github.com> Date: Fri, 21 Aug 2026 05:14:33 +0200 Subject: [PATCH 2/3] fix(ux): use title case for button labels (#40048) Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Evan Rusackas --- .../playwright/components/modals/ConfirmDialog.ts | 6 +++--- .../src/SqlLab/components/ResultSet/index.tsx | 2 +- .../Datasource/DatasourceModal/DatasourceModal.test.tsx | 8 ++++---- .../src/components/Datasource/DatasourceModal/index.tsx | 2 +- .../AnnotationLayerControl/AnnotationLayer.test.tsx | 8 ++++---- .../controls/AnnotationLayerControl/AnnotationLayer.tsx | 2 +- .../controls/DatasourceControl/DatasourceControl.test.tsx | 6 +++--- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/superset-frontend/playwright/components/modals/ConfirmDialog.ts b/superset-frontend/playwright/components/modals/ConfirmDialog.ts index 2d1c975e1709..c62ae7bbbb39 100644 --- a/superset-frontend/playwright/components/modals/ConfirmDialog.ts +++ b/superset-frontend/playwright/components/modals/ConfirmDialog.ts @@ -22,7 +22,7 @@ import { Modal } from '../core/Modal'; /** * Confirm Dialog component for Ant Design Modal.confirm dialogs. - * These are the "OK" / "Cancel" confirmation dialogs used throughout Superset. + * These are the "Confirm" / "Cancel" confirmation dialogs used throughout Superset. * Uses getByRole with name to target specific confirm dialogs when multiple are open. */ export class ConfirmDialog extends Modal { @@ -43,7 +43,7 @@ export class ConfirmDialog extends Modal { } /** - * Clicks the OK button to confirm. + * Clicks the Confirm button to confirm. * @param options.timeout - If provided, silently returns if dialog doesn't appear * within timeout. If not provided, waits indefinitely (strict mode). */ @@ -53,7 +53,7 @@ export class ConfirmDialog extends Modal { state: 'visible', timeout: options?.timeout, }); - await this.clickFooterButton('OK'); + await this.clickFooterButton('Confirm'); await this.waitForHidden(); } catch (error) { // Only swallow TimeoutError when timeout was explicitly provided diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx index a4c24a34c805..18786158c123 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx @@ -390,7 +390,7 @@ const ResultSet = ({ // provides. redirect(getExportCsvUrl(query.id)); }, - confirmText: t('OK'), + confirmText: t('Confirm'), cancelText: t('Close'), }); } diff --git a/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.tsx b/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.tsx index b5b7a1a6b3c0..942cddb6648d 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.tsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.tsx @@ -120,7 +120,7 @@ describe('DatasourceModal', () => { }); const saveButton = screen.getByTestId('datasource-modal-save'); fireEvent.click(saveButton); - const okButton = await screen.findByRole('button', { name: 'OK' }); + const okButton = await screen.findByRole('button', { name: 'Confirm' }); fireEvent.click(okButton); await waitFor(() => { expect(onDatasourceSave).toHaveBeenCalled(); @@ -142,7 +142,7 @@ describe('DatasourceModal', () => { const saveButton = screen.getByTestId('datasource-modal-save'); fireEvent.click(saveButton); - const okButton = await screen.findByRole('button', { name: 'OK' }); + const okButton = await screen.findByRole('button', { name: 'Confirm' }); fireEvent.click(okButton); const errorElements = await screen.findAllByText('Error saving dataset'); @@ -230,7 +230,7 @@ describe('DatasourceModal', () => { expect(checkbox).toBeChecked(); // Click OK to submit - const okButton = screen.getByRole('button', { name: 'OK' }); + const okButton = screen.getByRole('button', { name: 'Confirm' }); fireEvent.click(okButton); // Verify the PUT request was made with override_columns=true @@ -297,7 +297,7 @@ describe('DatasourceModal', () => { expect(checkbox).not.toBeChecked(); // Click OK to submit - const okButton = screen.getByRole('button', { name: 'OK' }); + const okButton = screen.getByRole('button', { name: 'Confirm' }); fireEvent.click(okButton); // Verify the PUT request was made with override_columns=false diff --git a/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx b/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx index db06d3da57a5..69c7ff293da8 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx @@ -395,7 +395,7 @@ const DatasourceModal: FunctionComponent = ({ show={confirmModalOpen} onHide={handleConfirmModalClose} onHandledPrimaryAction={handleConfirmSave} - primaryButtonName={t('OK')} + primaryButtonName={t('Confirm')} primaryButtonLoading={isSaving} > {getSaveDialog()} diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx index fbad84786206..c557c2f90eaf 100644 --- a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx +++ b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx @@ -150,7 +150,7 @@ const waitForRender = (props?: any) => test('renders with default props', async () => { await waitForRender(); expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled(); - expect(screen.getByRole('button', { name: 'OK' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Confirm' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Cancel' })).toBeEnabled(); }); @@ -188,7 +188,7 @@ test('enables apply and ok buttons', async () => { await waitFor(() => { expect(screen.getByRole('button', { name: 'Apply' })).toBeEnabled(); - expect(screen.getByRole('button', { name: 'OK' })).toBeEnabled(); + expect(screen.getByRole('button', { name: 'Confirm' })).toBeEnabled(); }); }); @@ -203,7 +203,7 @@ test('triggers addAnnotationLayer and close when ok button is clicked', async () const addAnnotationLayer = jest.fn(); const close = jest.fn(); await waitForRender({ name: 'Test', value: '2x', addAnnotationLayer, close }); - userEvent.click(screen.getByRole('button', { name: 'OK' })); + userEvent.click(screen.getByRole('button', { name: 'Confirm' })); expect(addAnnotationLayer).toHaveBeenCalled(); expect(close).toHaveBeenCalled(); }); @@ -724,7 +724,7 @@ test('Disable apply button if formula is incorrect', async () => { const formulaInput = screen.getByRole('textbox', { name: 'Formula' }); const applyButton = screen.getByRole('button', { name: 'Apply' }); - const okButton = screen.getByRole('button', { name: 'OK' }); + const okButton = screen.getByRole('button', { name: 'Confirm' }); userEvent.type(formulaInput, 'x+1'); expect(formulaInput).toHaveValue('x+1'); diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.tsx b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.tsx index 9f1b9abb33fc..b429151c6604 100644 --- a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.tsx +++ b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.tsx @@ -1303,7 +1303,7 @@ function AnnotationLayer({ disabled={!isValid} onClick={submitAnnotation} > - {t('OK')} + {t('Confirm')} diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx index 98debbb906e0..07d6365f6c62 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx @@ -187,7 +187,7 @@ async function openAndSaveChanges( await userEvent.click(screen.getByTestId('datasource-menu-trigger')); await userEvent.click(await screen.findByTestId('edit-dataset')); await userEvent.click(await screen.findByTestId('datasource-modal-save')); - await userEvent.click(await screen.findByText('OK')); + await userEvent.click(await screen.findByText('Confirm')); } test('Should render', async () => { @@ -714,10 +714,10 @@ test('should handle metric save confirmation modal', async () => { await userEvent.click(await screen.findByTestId('datasource-modal-save')); // Verify confirmation modal appears - expect(await screen.findByText('OK')).toBeInTheDocument(); + expect(await screen.findByText('Confirm')).toBeInTheDocument(); // Confirm save - await userEvent.click(screen.getByText('OK')); + await userEvent.click(screen.getByText('Confirm')); await waitFor(() => { expect(props.onDatasourceSave).toHaveBeenCalled(); From f7d505e1fd0b4d0fbac4fe90637815343c867628 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Thu, 20 Aug 2026 23:23:46 -0400 Subject: [PATCH 3/3] fix(listview): stop card clicks creating a duplicate history entry (#43310) --- .../src/features/charts/ChartCard.test.tsx | 108 ++++++++++++++++++ .../src/features/charts/ChartCard.tsx | 14 ++- .../dashboards/DashboardCard.test.tsx | 51 ++++++++- .../src/features/dashboards/DashboardCard.tsx | 6 +- .../src/views/CRUD/utils.test.tsx | 32 ++++++ superset-frontend/src/views/CRUD/utils.tsx | 12 ++ 6 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 superset-frontend/src/features/charts/ChartCard.test.tsx diff --git a/superset-frontend/src/features/charts/ChartCard.test.tsx b/superset-frontend/src/features/charts/ChartCard.test.tsx new file mode 100644 index 000000000000..eb0134e92bc1 --- /dev/null +++ b/superset-frontend/src/features/charts/ChartCard.test.tsx @@ -0,0 +1,108 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { createMemoryHistory, type Update } from 'history'; +import { Router } from 'react-router-dom'; +import { isFeatureEnabled } from '@superset-ui/core'; +import { render, screen, fireEvent } from 'spec/helpers/testing-library'; +import type Chart from 'src/types/Chart'; +import ChartCard from './ChartCard'; + +jest.mock('@superset-ui/core', () => ({ + ...jest.requireActual('@superset-ui/core'), + isFeatureEnabled: jest.fn(), +})); + +const mockChart = { + id: 1, + slice_name: 'Sample Chart', + url: '/explore/?slice_id=1', + changed_on_delta_humanized: '2 days ago', + datasource_name_text: 'Sample dataset', + thumbnail_url: '/thumbnail.png', +} as Chart; + +const renderCard = (history: ReturnType) => + render( + + true} + openChartEditModal={jest.fn()} + bulkSelectEnabled={false} + addDangerToast={jest.fn()} + addSuccessToast={jest.fn()} + refreshData={jest.fn()} + saveFavoriteStatus={jest.fn()} + favoriteStatus={false} + showThumbnails + handleBulkChartExport={jest.fn()} + /> + , + ); + +const recordNavigations = ( + history: ReturnType, +): string[] => { + const navigations: string[] = []; + history.listen(({ action, location }: Update) => + navigations.push(`${action} ${location.pathname}${location.search}`), + ); + return navigations; +}; + +beforeEach(() => { + (isFeatureEnabled as jest.Mock).mockReturnValue(true); +}); + +afterEach(() => { + (isFeatureEnabled as jest.Mock).mockReset(); +}); + +test('renders the chart title', () => { + renderCard(createMemoryHistory()); + expect(screen.getByText('Sample Chart')).toBeInTheDocument(); +}); + +test('clicking the thumbnail navigates to the chart exactly once', () => { + // The cover is a router link and the whole card is clickable, so a click on + // the cover used to be handled twice and pushed two identical entries. That + // left the Back button popping the duplicate instead of returning the user to + // the page they came from. + const history = createMemoryHistory({ + initialEntries: ['/superset/welcome/'], + }); + renderCard(history); + const navigations = recordNavigations(history); + + fireEvent.click(screen.getByRole('link')); + + expect(navigations).toEqual(['PUSH /explore/?slice_id=1']); +}); + +test('clicking the card outside the thumbnail navigates to the chart', () => { + const history = createMemoryHistory({ + initialEntries: ['/superset/welcome/'], + }); + renderCard(history); + const navigations = recordNavigations(history); + + fireEvent.click(screen.getByText('Sample Chart')); + + expect(navigations).toEqual(['PUSH /explore/?slice_id=1']); +}); diff --git a/superset-frontend/src/features/charts/ChartCard.tsx b/superset-frontend/src/features/charts/ChartCard.tsx index 4dca38b9839a..a4c4d47bf79d 100644 --- a/superset-frontend/src/features/charts/ChartCard.tsx +++ b/superset-frontend/src/features/charts/ChartCard.tsx @@ -32,7 +32,11 @@ import { import Chart from 'src/types/Chart'; import { SubjectPile } from 'src/features/subjects/SubjectPile'; import { KebabMenuButton } from 'src/components'; -import { handleChartDelete, CardStyles } from 'src/views/CRUD/utils'; +import { + handleChartDelete, + CardStyles, + isNavigationHandledByLink, +} from 'src/views/CRUD/utils'; import { assetUrl } from 'src/utils/assetUrl'; import type { ListViewFetchDataConfig as FetchDataConfig } from 'src/components'; import { TableTab } from 'src/views/CRUD/types'; @@ -208,8 +212,12 @@ export default function ChartCard({ return ( { - if (!bulkSelectEnabled && chart.url) { + onClick={event => { + if ( + !bulkSelectEnabled && + chart.url && + !isNavigationHandledByLink(event) + ) { history.push(chart.url); } }} diff --git a/superset-frontend/src/features/dashboards/DashboardCard.test.tsx b/superset-frontend/src/features/dashboards/DashboardCard.test.tsx index dbb7364f2724..9b17eb66ba41 100644 --- a/superset-frontend/src/features/dashboards/DashboardCard.test.tsx +++ b/superset-frontend/src/features/dashboards/DashboardCard.test.tsx @@ -17,10 +17,16 @@ * under the License. */ -import { MemoryRouter } from 'react-router-dom'; +import { createMemoryHistory, type Update } from 'history'; +import { MemoryRouter, Router } from 'react-router-dom'; import { isFeatureEnabled } from '@superset-ui/core'; -import { render, screen } from 'spec/helpers/testing-library'; +import { + render, + screen, + fireEvent, + within, +} from 'spec/helpers/testing-library'; import { SubjectType } from 'src/types/Subject'; import DashboardCard from './DashboardCard'; @@ -63,6 +69,10 @@ afterAll(() => { mockedIsFeatureEnabled.mockClear(); }); +afterEach(() => { + jest.restoreAllMocks(); +}); + beforeEach(() => { render( @@ -101,6 +111,43 @@ test('Renders the modified date', () => { expect(modifiedDateElement).toBeInTheDocument(); }); +test('clicking the thumbnail navigates to the dashboard exactly once', () => { + // The cover is a router link and the whole card is clickable, so a click on + // the cover used to be handled twice and pushed two identical entries, which + // left the Back button popping the duplicate rather than returning the user + // to the page they came from. + jest.spyOn(global, 'fetch').mockResolvedValue({ + blob: () => Promise.resolve(new Blob([''], { type: 'image/png' })), + } as Response); + const history = createMemoryHistory({ + initialEntries: ['/superset/welcome/'], + }); + const { container } = render( + + + , + ); + const navigations: string[] = []; + history.listen(({ action, location }: Update) => + navigations.push(`${action} ${location.pathname}`), + ); + + fireEvent.click(within(container).getByRole('link')); + + expect(navigations).toEqual(['PUSH /dashboard/1']); +}); + describe('thumbnail URL construction', () => { let fetchSpy: jest.SpyInstance; diff --git a/superset-frontend/src/features/dashboards/DashboardCard.tsx b/superset-frontend/src/features/dashboards/DashboardCard.tsx index 642b55cf0100..63e854a388a0 100644 --- a/superset-frontend/src/features/dashboards/DashboardCard.tsx +++ b/superset-frontend/src/features/dashboards/DashboardCard.tsx @@ -20,7 +20,7 @@ import { Link, useHistory } from 'react-router-dom'; import { t } from '@apache-superset/core/translation'; import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core'; import { css } from '@apache-superset/core/theme'; -import { CardStyles } from 'src/views/CRUD/utils'; +import { CardStyles, isNavigationHandledByLink } from 'src/views/CRUD/utils'; import { FaveStar, Icons, @@ -169,8 +169,8 @@ function DashboardCard({ return ( { - if (!bulkSelectEnabled) { + onClick={event => { + if (!bulkSelectEnabled && !isNavigationHandledByLink(event)) { history.push(dashboard.url); } }} diff --git a/superset-frontend/src/views/CRUD/utils.test.tsx b/superset-frontend/src/views/CRUD/utils.test.tsx index 4555373a6565..4eee1c5b53c3 100644 --- a/superset-frontend/src/views/CRUD/utils.test.tsx +++ b/superset-frontend/src/views/CRUD/utils.test.tsx @@ -28,6 +28,7 @@ import { getSSHPrivateKeyPasswordsNeeded, hasTerminalValidation, isAlreadyExists, + isNavigationHandledByLink, isNeedsEncryptedExtraField, isNeedsPassword, isNeedsSSHPassword, @@ -259,6 +260,37 @@ const encryptedExtraFieldNoLabelErrors = { ], }; +test('identifies clicks a link has already navigated', () => { + document.body.innerHTML = ` +
+ + Chart + Label +
+ `; + const target = (id: string) => ({ target: document.getElementById(id) }); + + // the link itself and anything nested inside it + expect(isNavigationHandledByLink(target('cover'))).toBe(true); + expect(isNavigationHandledByLink(target('thumbnail'))).toBe(true); + + // the rest of the card still navigates through its own click handler + expect(isNavigationHandledByLink(target('title'))).toBe(false); + expect(isNavigationHandledByLink(target('card'))).toBe(false); + + // an anchor with no href does not navigate, so it must not suppress the card + expect(isNavigationHandledByLink(target('anchorWithoutHref'))).toBe(false); + expect(isNavigationHandledByLink(target('inertLabel'))).toBe(false); + + // targets that are not elements + expect(isNavigationHandledByLink({ target: null })).toBe(false); + expect( + isNavigationHandledByLink({ target: document.createTextNode('text') }), + ).toBe(false); + + document.body.innerHTML = ''; +}); + test('identifies error payloads indicating that password is needed', () => { let needsPassword; diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx index 5503f143e985..f429572e137e 100644 --- a/superset-frontend/src/views/CRUD/utils.tsx +++ b/superset-frontend/src/views/CRUD/utils.tsx @@ -483,6 +483,18 @@ export const CardStyles = styled.div` } `; +/** + * Cards make their whole surface clickable, but `ListViewCard` also renders its + * cover as a router ``. A click on the cover is therefore handled twice — + * once by the link and once by the card wrapper — pushing two identical history + * entries for a single click, so the Back button only pops the duplicate and + * leaves the user on the page they tried to leave. Let the link win in that case. + */ +export const isNavigationHandledByLink = (event: { + target: EventTarget | null; +}): boolean => + Boolean((event.target as HTMLElement | null)?.closest?.('a[href]')); + export /* eslint-disable no-underscore-dangle */ const isNeedsPassword = (payload: any) => typeof payload === 'object' &&