mirror of
https://github.com/datahub-project/datahub.git
synced 2025-10-19 12:53:28 +00:00
fix(ui): open links from inside modal in a new tab (#14431)
This commit is contained in:
parent
4e83f951be
commit
8202977c74
@ -3,6 +3,8 @@ import { Modal as AntModal, ModalProps as AntModalProps } from 'antd';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { ModalContext } from '@app/sharedV2/modals/ModalContext';
|
||||||
|
|
||||||
const StyledModal = styled(AntModal)<{ hasChildren: boolean }>`
|
const StyledModal = styled(AntModal)<{ hasChildren: boolean }>`
|
||||||
font-family: ${typography.fonts.body};
|
font-family: ${typography.fonts.body};
|
||||||
|
|
||||||
@ -125,7 +127,7 @@ export function Modal({
|
|||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
<ModalContext.Provider value={{ isInsideModal: true }}>{children}</ModalContext.Provider>
|
||||||
</StyledModal>
|
</StyledModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
import analytics, { EventType } from '@app/analytics';
|
import analytics, { EventType } from '@app/analytics';
|
||||||
import AutoCompleteEntityItem from '@app/searchV2/autoCompleteV2/AutoCompleteEntityItem';
|
import AutoCompleteEntityItem from '@app/searchV2/autoCompleteV2/AutoCompleteEntityItem';
|
||||||
|
import { useGetModalLinkProps } from '@app/sharedV2/modals/useGetModalLinkProps';
|
||||||
import { useEntityRegistryV2 } from '@app/useEntityRegistry';
|
import { useEntityRegistryV2 } from '@app/useEntityRegistry';
|
||||||
|
|
||||||
import { DataHubPageModuleType, Entity } from '@types';
|
import { DataHubPageModuleType, Entity } from '@types';
|
||||||
@ -34,6 +35,7 @@ export default function EntityItem({
|
|||||||
padding,
|
padding,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const entityRegistry = useEntityRegistryV2();
|
const entityRegistry = useEntityRegistryV2();
|
||||||
|
const linkProps = useGetModalLinkProps();
|
||||||
|
|
||||||
const sendAnalytics = useCallback(
|
const sendAnalytics = useCallback(
|
||||||
() =>
|
() =>
|
||||||
@ -60,7 +62,11 @@ export default function EntityItem({
|
|||||||
onClick={sendAnalytics}
|
onClick={sendAnalytics}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<StyledLink to={entityRegistry.getEntityUrl(entity.type, entity.urn)} onClick={sendAnalytics}>
|
<StyledLink
|
||||||
|
to={entityRegistry.getEntityUrl(entity.type, entity.urn)}
|
||||||
|
onClick={sendAnalytics}
|
||||||
|
{...linkProps}
|
||||||
|
>
|
||||||
<AutoCompleteEntityItem
|
<AutoCompleteEntityItem
|
||||||
entity={entity}
|
entity={entity}
|
||||||
key={entity.urn}
|
key={entity.urn}
|
||||||
|
@ -12,6 +12,7 @@ import EntitySubtitle from '@app/searchV2/autoCompleteV2/components/subtitle/Ent
|
|||||||
import { VARIANT_STYLES } from '@app/searchV2/autoCompleteV2/constants';
|
import { VARIANT_STYLES } from '@app/searchV2/autoCompleteV2/constants';
|
||||||
import { EntityItemVariant } from '@app/searchV2/autoCompleteV2/types';
|
import { EntityItemVariant } from '@app/searchV2/autoCompleteV2/types';
|
||||||
import { getEntityDisplayType } from '@app/searchV2/autoCompleteV2/utils';
|
import { getEntityDisplayType } from '@app/searchV2/autoCompleteV2/utils';
|
||||||
|
import { useGetModalLinkProps } from '@app/sharedV2/modals/useGetModalLinkProps';
|
||||||
import { Text } from '@src/alchemy-components';
|
import { Text } from '@src/alchemy-components';
|
||||||
import { useEntityRegistryV2 } from '@src/app/useEntityRegistry';
|
import { useEntityRegistryV2 } from '@src/app/useEntityRegistry';
|
||||||
import { Entity, MatchedField } from '@src/types.generated';
|
import { Entity, MatchedField } from '@src/types.generated';
|
||||||
@ -131,6 +132,8 @@ export default function AutoCompleteEntityItem({
|
|||||||
}: EntityAutocompleteItemProps) {
|
}: EntityAutocompleteItemProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const entityRegistry = useEntityRegistryV2();
|
const entityRegistry = useEntityRegistryV2();
|
||||||
|
const linkProps = useGetModalLinkProps();
|
||||||
|
|
||||||
const displayName = entityRegistry.getDisplayName(entity.type, entity);
|
const displayName = entityRegistry.getDisplayName(entity.type, entity);
|
||||||
const displayType = getEntityDisplayType(entity, entityRegistry);
|
const displayType = getEntityDisplayType(entity, entityRegistry);
|
||||||
const variantProps = VARIANT_STYLES.get(variant ?? 'default');
|
const variantProps = VARIANT_STYLES.get(variant ?? 'default');
|
||||||
@ -140,7 +143,7 @@ export default function AutoCompleteEntityItem({
|
|||||||
: DisplayNameHoverFromContainer;
|
: DisplayNameHoverFromContainer;
|
||||||
|
|
||||||
const displayNameContent = variantProps?.nameCanBeHovered ? (
|
const displayNameContent = variantProps?.nameCanBeHovered ? (
|
||||||
<Link to={entityRegistry.getEntityUrl(entity.type, entity.urn)}>
|
<Link to={entityRegistry.getEntityUrl(entity.type, entity.urn)} {...linkProps}>
|
||||||
<DisplayNameHoverComponent
|
<DisplayNameHoverComponent
|
||||||
displayName={displayName}
|
displayName={displayName}
|
||||||
highlight={query}
|
highlight={query}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useModalContext } from '@app/sharedV2/modals/ModalContext';
|
||||||
import { PageRoutes } from '@conf/Global';
|
import { PageRoutes } from '@conf/Global';
|
||||||
|
|
||||||
// Function to check if the current page is an embedded profile
|
// Function to check if the current page is an embedded profile
|
||||||
@ -11,5 +12,9 @@ export const useIsEmbeddedProfile = () => {
|
|||||||
|
|
||||||
export const useEmbeddedProfileLinkProps = () => {
|
export const useEmbeddedProfileLinkProps = () => {
|
||||||
const isEmbedded = useIsEmbeddedProfile();
|
const isEmbedded = useIsEmbeddedProfile();
|
||||||
return useMemo(() => (isEmbedded ? { target: '_blank', rel: 'noreferrer noopener' } : {}), [isEmbedded]);
|
const { isInsideModal } = useModalContext(); // If link is opened from inside a modal
|
||||||
|
return useMemo(
|
||||||
|
() => (isEmbedded || isInsideModal ? { target: '_blank', rel: 'noreferrer noopener' } : {}),
|
||||||
|
[isEmbedded, isInsideModal],
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
export type ModalContextType = {
|
||||||
|
isInsideModal: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModalContext = createContext<ModalContextType>({ isInsideModal: false });
|
||||||
|
|
||||||
|
export const useModalContext = () => useContext(ModalContext);
|
@ -0,0 +1,83 @@
|
|||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import React, { PropsWithChildren } from 'react';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { ModalContext, ModalContextType } from '@app/sharedV2/modals/ModalContext';
|
||||||
|
import { useGetModalLinkProps } from '@app/sharedV2/modals/useGetModalLinkProps';
|
||||||
|
|
||||||
|
function wrapperWithContext(value: ModalContextType) {
|
||||||
|
return ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<ModalContext.Provider value={value}>{children}</ModalContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useGetModalLinkProps', () => {
|
||||||
|
it('should return an empty object when not in modal (default context)', () => {
|
||||||
|
const { result } = renderHook(() => useGetModalLinkProps());
|
||||||
|
expect(result.current).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct props when isInsideModal = true', () => {
|
||||||
|
const { result } = renderHook(() => useGetModalLinkProps(), {
|
||||||
|
wrapper: wrapperWithContext({ isInsideModal: true }),
|
||||||
|
});
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener noreferrer',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty object when isInsideModal = false', () => {
|
||||||
|
const { result } = renderHook(() => useGetModalLinkProps(), {
|
||||||
|
wrapper: wrapperWithContext({ isInsideModal: false }),
|
||||||
|
});
|
||||||
|
expect(result.current).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should memoize result for same context value', () => {
|
||||||
|
const { result, rerender } = renderHook(() => useGetModalLinkProps(), {
|
||||||
|
wrapper: wrapperWithContext({ isInsideModal: true }),
|
||||||
|
});
|
||||||
|
const first = result.current;
|
||||||
|
rerender();
|
||||||
|
expect(result.current).toBe(first); // stable reference
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change reference when context value changes', () => {
|
||||||
|
const wrapper = ({ children, value }: PropsWithChildren<{ value: boolean }>) => (
|
||||||
|
<ModalContext.Provider value={{ isInsideModal: value }}>{children}</ModalContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { result, rerender } = renderHook(() => useGetModalLinkProps(), {
|
||||||
|
initialProps: { value: false },
|
||||||
|
wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
const first = result.current;
|
||||||
|
rerender({ value: true });
|
||||||
|
expect(result.current).not.toBe(first);
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener noreferrer',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have no extra keys when not in modal', () => {
|
||||||
|
const { result } = renderHook(() => useGetModalLinkProps(), {
|
||||||
|
wrapper: wrapperWithContext({ isInsideModal: false }),
|
||||||
|
});
|
||||||
|
expect(Object.keys(result.current)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match snapshot when in modal', () => {
|
||||||
|
const { result } = renderHook(() => useGetModalLinkProps(), {
|
||||||
|
wrapper: wrapperWithContext({ isInsideModal: true }),
|
||||||
|
});
|
||||||
|
expect(result.current).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"rel": "noopener noreferrer",
|
||||||
|
"target": "_blank",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,14 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useModalContext } from '@app/sharedV2/modals/ModalContext';
|
||||||
|
|
||||||
|
export const useGetModalLinkProps = () => {
|
||||||
|
const { isInsideModal } = useModalContext();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (isInsideModal) {
|
||||||
|
return { target: '_blank', rel: 'noopener noreferrer' };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}, [isInsideModal]);
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user