diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentExternalLinkModal.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentExternalLinkModal.tsx
new file mode 100644
index 0000000000..5aa826f579
--- /dev/null
+++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentExternalLinkModal.tsx
@@ -0,0 +1,153 @@
+import { styled } from '@linaria/react';
+import { t } from '@lingui/core/macro';
+import { useId } from 'react';
+import { Button, Checkbox } from 'twenty-ui/input';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+import { H1Title, H1TitleFontColor } from 'twenty-ui/typography';
+
+import { FRONT_COMPONENT_EXTERNAL_LINK_MODAL_ID } from '@/front-components/constants/FrontComponentExternalLinkModalId';
+import { getExternalLinkDisplayUrl } from '@/front-components/utils/getExternalLinkDisplayUrl';
+import { ModalStatefulWrapper } from '@/ui/layout/modal/components/ModalStatefulWrapper';
+import { useModal } from '@/ui/layout/modal/hooks/useModal';
+
+const FRONT_COMPONENT_EXTERNAL_LINK_MODAL_WIDTH = 320;
+
+const StyledCenteredTitle = styled.div`
+ text-align: center;
+
+ h2 {
+ margin-bottom: 0;
+ }
+`;
+
+const StyledDestinationUrl = styled.span`
+ align-items: center;
+ align-self: center;
+ background-color: ${themeCssVariables.background.transparent.lighter};
+ border: 1px solid ${themeCssVariables.border.color.strong};
+ border-radius: ${themeCssVariables.border.radius.pill};
+ color: ${themeCssVariables.font.color.primary};
+ corner-shape: round;
+ display: inline-flex;
+ font-size: ${themeCssVariables.font.size.md};
+ height: 20px;
+ justify-content: center;
+ max-width: 100%;
+ overflow: hidden;
+ padding: 0 ${themeCssVariables.spacing[2]};
+`;
+
+const StyledDestinationUrlText = styled.span`
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`;
+
+const StyledActions = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[2]};
+`;
+
+const StyledTrustOriginRow = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing[1]};
+ justify-content: center;
+`;
+
+const StyledTrustOriginLabel = styled.span`
+ color: ${themeCssVariables.font.color.primary};
+ cursor: pointer;
+ font-size: ${themeCssVariables.font.size.sm};
+`;
+
+type FrontComponentExternalLinkModalProps = {
+ url: string;
+ shouldTrustOrigin: boolean;
+ onShouldTrustOriginChange: (shouldTrustOrigin: boolean) => void;
+ onConfirm: () => void;
+ onClose: () => void;
+};
+
+export const FrontComponentExternalLinkModal = ({
+ url,
+ shouldTrustOrigin,
+ onShouldTrustOriginChange,
+ onConfirm,
+ onClose,
+}: FrontComponentExternalLinkModalProps) => {
+ const { closeModal } = useModal();
+ const trustOriginLabelId = useId();
+
+ const handleConfirmClick = () => {
+ closeModal(FRONT_COMPONENT_EXTERNAL_LINK_MODAL_ID);
+ onConfirm();
+ };
+
+ const handleCancelClick = () => {
+ closeModal(FRONT_COMPONENT_EXTERNAL_LINK_MODAL_ID);
+ onClose();
+ };
+
+ return (
+
+
+
+
+
+
+ {getExternalLinkDisplayUrl(url)}
+
+
+
+
+
+ onShouldTrustOriginChange(!shouldTrustOrigin)}
+ >
+ {t`Always allow links to this domain`}
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentExternalLinkModalManager.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentExternalLinkModalManager.tsx
index d63697359c..1a4e272753 100644
--- a/packages/twenty-front/src/modules/front-components/components/FrontComponentExternalLinkModalManager.tsx
+++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentExternalLinkModalManager.tsx
@@ -1,13 +1,10 @@
-import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
-import { FrontComponentExternalLinkModalSubtitle } from '@/front-components/components/FrontComponentExternalLinkModalSubtitle';
-import { FRONT_COMPONENT_EXTERNAL_LINK_MODAL_ID } from '@/front-components/constants/FrontComponentExternalLinkModalId';
+import { FrontComponentExternalLinkModal } from '@/front-components/components/FrontComponentExternalLinkModal';
import { frontComponentExternalLinkModalConfigState } from '@/front-components/states/frontComponentExternalLinkModalConfigState';
import { trustedFrontComponentExternalOriginsState } from '@/front-components/states/trustedFrontComponentExternalOriginsState';
import { openExternalUrl } from '@/front-components/utils/openExternalUrl';
-import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
@@ -29,7 +26,7 @@ export const FrontComponentExternalLinkModalManager = () => {
const { applicationId, url, origin } = frontComponentExternalLinkModalConfig;
- const handleConfirmClick = () => {
+ const handleConfirm = () => {
if (shouldTrustOrigin) {
setTrustedFrontComponentExternalOrigins((previousTrustedOrigins) => ({
...previousTrustedOrigins,
@@ -51,20 +48,11 @@ export const FrontComponentExternalLinkModalManager = () => {
};
return (
-
- }
- confirmButtonText={t`Continue`}
- confirmButtonAccent="blue"
- onConfirmClick={handleConfirmClick}
+
);
diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentExternalLinkModalSubtitle.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentExternalLinkModalSubtitle.tsx
deleted file mode 100644
index a946b04d38..0000000000
--- a/packages/twenty-front/src/modules/front-components/components/FrontComponentExternalLinkModalSubtitle.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { styled } from '@linaria/react';
-import { Trans } from '@lingui/react/macro';
-import { useId } from 'react';
-import { Checkbox } from 'twenty-ui/input';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
-
-const StyledContent = styled.div`
- align-items: center;
- display: flex;
- flex-direction: column;
- gap: ${themeCssVariables.spacing[4]};
- overflow-wrap: anywhere;
-`;
-
-const StyledTrustRow = styled.div`
- align-items: center;
- align-self: stretch;
- display: flex;
- gap: ${themeCssVariables.spacing[2]};
- text-align: left;
-`;
-
-const StyledTrustLabel = styled.span`
- cursor: pointer;
-`;
-
-type FrontComponentExternalLinkModalSubtitleProps = {
- url: string;
- origin: string;
- shouldTrustOrigin: boolean;
- onShouldTrustOriginChange: (shouldTrustOrigin: boolean) => void;
-};
-
-export const FrontComponentExternalLinkModalSubtitle = ({
- url,
- origin,
- shouldTrustOrigin,
- onShouldTrustOriginChange,
-}: FrontComponentExternalLinkModalSubtitleProps) => {
- const trustOriginLabelId = useId();
-
- return (
-
-
-
- This link will take you to an external site: {url}
-
-
-
-
- onShouldTrustOriginChange(!shouldTrustOrigin)}
- >
-
- Don't ask again for {origin}
-
-
-
-
- );
-};
diff --git a/packages/twenty-front/src/modules/front-components/components/__stories__/FrontComponentExternalLinkModal.stories.tsx b/packages/twenty-front/src/modules/front-components/components/__stories__/FrontComponentExternalLinkModal.stories.tsx
new file mode 100644
index 0000000000..2eb7b9dc56
--- /dev/null
+++ b/packages/twenty-front/src/modules/front-components/components/__stories__/FrontComponentExternalLinkModal.stories.tsx
@@ -0,0 +1,72 @@
+import {
+ type Decorator,
+ type Meta,
+ type StoryObj,
+} from '@storybook/react-vite';
+import { fn } from 'storybook/test';
+
+import { FrontComponentExternalLinkModal } from '@/front-components/components/FrontComponentExternalLinkModal';
+import { FRONT_COMPONENT_EXTERNAL_LINK_MODAL_ID } from '@/front-components/constants/FrontComponentExternalLinkModalId';
+import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
+import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
+import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
+import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
+import { ComponentDecorator } from 'twenty-ui/testing';
+import { RootDecorator } from '~/testing/decorators/RootDecorator';
+
+const OpenedModalDecorator: Decorator = (Story) => {
+ jotaiStore.set(
+ isModalOpenedComponentState.atomFamily({
+ instanceId: FRONT_COMPONENT_EXTERNAL_LINK_MODAL_ID,
+ }),
+ true,
+ );
+ jotaiStore.set(focusStackState.atom, [
+ {
+ focusId: FRONT_COMPONENT_EXTERNAL_LINK_MODAL_ID,
+ componentInstance: {
+ componentType: FocusComponentType.MODAL,
+ componentInstanceId: FRONT_COMPONENT_EXTERNAL_LINK_MODAL_ID,
+ },
+ globalHotkeysConfig: {
+ enableGlobalHotkeysWithModifiers: true,
+ enableGlobalHotkeysConflictingWithKeyboard: true,
+ },
+ },
+ ]);
+
+ return ;
+};
+
+const meta: Meta = {
+ title: 'Modules/FrontComponents/FrontComponentExternalLinkModal',
+ component: FrontComponentExternalLinkModal,
+ decorators: [OpenedModalDecorator, RootDecorator, ComponentDecorator],
+ parameters: {
+ disableHotkeyInitialization: true,
+ },
+ args: {
+ url: 'https://nvidia.com',
+ shouldTrustOrigin: true,
+ onShouldTrustOriginChange: fn(),
+ onConfirm: fn(),
+ onClose: fn(),
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const WithTrustedOriginUnchecked: Story = {
+ args: {
+ shouldTrustOrigin: false,
+ },
+};
+
+export const WithLongUrl: Story = {
+ args: {
+ url: 'https://developer.nvidia.com/blog/category/generative-ai/very-long-article-slug?utm_source=twenty',
+ },
+};
diff --git a/packages/twenty-front/src/modules/front-components/utils/__tests__/getExternalLinkDisplayUrl.test.ts b/packages/twenty-front/src/modules/front-components/utils/__tests__/getExternalLinkDisplayUrl.test.ts
new file mode 100644
index 0000000000..fd8201a286
--- /dev/null
+++ b/packages/twenty-front/src/modules/front-components/utils/__tests__/getExternalLinkDisplayUrl.test.ts
@@ -0,0 +1,29 @@
+import { getExternalLinkDisplayUrl } from '@/front-components/utils/getExternalLinkDisplayUrl';
+
+describe('getExternalLinkDisplayUrl', () => {
+ it('should strip the scheme and the trailing slash of a bare domain', () => {
+ expect(getExternalLinkDisplayUrl('https://nvidia.com/')).toBe('nvidia.com');
+ });
+
+ it('should strip the www subdomain', () => {
+ expect(getExternalLinkDisplayUrl('https://www.nvidia.com')).toBe(
+ 'nvidia.com',
+ );
+ });
+
+ it('should keep the path, the search params and the hash', () => {
+ expect(
+ getExternalLinkDisplayUrl('https://nvidia.com/drivers?os=mac#latest'),
+ ).toBe('nvidia.com/drivers?os=mac#latest');
+ });
+
+ it('should keep the port', () => {
+ expect(getExternalLinkDisplayUrl('http://localhost:3000/app')).toBe(
+ 'localhost:3000/app',
+ );
+ });
+
+ it('should return the untouched value when the url cannot be parsed', () => {
+ expect(getExternalLinkDisplayUrl('not-a-url')).toBe('not-a-url');
+ });
+});
diff --git a/packages/twenty-front/src/modules/front-components/utils/getExternalLinkDisplayUrl.ts b/packages/twenty-front/src/modules/front-components/utils/getExternalLinkDisplayUrl.ts
new file mode 100644
index 0000000000..93361de874
--- /dev/null
+++ b/packages/twenty-front/src/modules/front-components/utils/getExternalLinkDisplayUrl.ts
@@ -0,0 +1,12 @@
+export const getExternalLinkDisplayUrl = (url: string) => {
+ try {
+ const { host, pathname, search, hash } = new URL(url);
+
+ const displayedHost = host.startsWith('www.') ? host.slice(4) : host;
+ const displayedPathname = pathname === '/' ? '' : pathname;
+
+ return `${displayedHost}${displayedPathname}${search}${hash}`;
+ } catch {
+ return url;
+ }
+};