diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/link/hooks/useAddLinkToNavigationMenuDraft.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/link/hooks/useAddLinkToNavigationMenuDraft.ts index ae5ea3e0895..82d2f43247f 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/link/hooks/useAddLinkToNavigationMenuDraft.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/link/hooks/useAddLinkToNavigationMenuDraft.ts @@ -1,12 +1,11 @@ import { NavigationMenuItemType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, normalizeUrl } from 'twenty-shared/utils'; import { v4 } from 'uuid'; import type { NavigationMenuItem } from '~/generated-metadata/graphql'; import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorLink'; import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition'; -import { normalizeUrl } from 'twenty-shared/utils'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; export const useAddLinkToNavigationMenuDraft = () => { diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityTableHook.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityTableHook.ts index 4c876344bb4..510d2c14c93 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityTableHook.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/spreadsheetImportGetUnicityTableHook.ts @@ -104,9 +104,7 @@ const getUniqueValues = ( .primaryLinkUrl, ) ) { - return normalizeUrlOrigin( - row?.[columnName]?.toString().trim() || '', - ); + return normalizeUrlOrigin(row?.[columnName]?.toString().trim() || ''); } return row?.[columnName]?.toString().trim().toLowerCase(); diff --git a/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-links-value.util.ts b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-links-value.util.ts index 7fc3ed7298f..03a7c8c3d0d 100644 --- a/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-links-value.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-links-value.util.ts @@ -1,11 +1,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import isEmpty from 'lodash.isempty'; import { type LinkMetadataNullable } from 'twenty-shared/types'; -import { - isDefined, - normalizeUrlOrigin, - parseJson, -} from 'twenty-shared/utils'; +import { isDefined, normalizeUrlOrigin, parseJson } from 'twenty-shared/utils'; import { removeEmptyLinks } from 'src/engine/core-modules/record-transformer/utils/remove-empty-links'; @@ -44,9 +40,7 @@ export const transformLinksValue = ( const processedSecondaryLinks = secondaryLinks?.map((link) => ({ ...link, - url: isDefined(link.url) - ? normalizeUrlOrigin(link.url) - : link.url, + url: isDefined(link.url) ? normalizeUrlOrigin(link.url) : link.url, })); return { diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts index ba8bf67e685..f6110770078 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts @@ -7,10 +7,7 @@ import { type ConnectedAccountProvider, type FieldActorSource, } from 'twenty-shared/types'; -import { - isDefined, - normalizeUrlOrigin, -} from 'twenty-shared/utils'; +import { isDefined, normalizeUrlOrigin } from 'twenty-shared/utils'; import { type DeepPartial, ILike } from 'typeorm'; import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service'; diff --git a/packages/twenty-shared/src/utils/url/__tests__/normalizeUrlOrigin.test.ts b/packages/twenty-shared/src/utils/url/__tests__/normalizeUrlOrigin.test.ts index f1e1a9685de..a6b83faf93d 100644 --- a/packages/twenty-shared/src/utils/url/__tests__/normalizeUrlOrigin.test.ts +++ b/packages/twenty-shared/src/utils/url/__tests__/normalizeUrlOrigin.test.ts @@ -39,19 +39,19 @@ describe('normalizeUrlOrigin', () => { expected: 'https://www.example.com/TEST#Hash', }, { - title: 'should preserve special characters in path', - input: 'https://test.test/edouard-ménard-22219837', - expected: 'https://test.test/edouard-ménard-22219837', + title: 'should percent-encode non-ASCII characters in path', + input: 'https://test.test/john-döe-22219837', + expected: 'https://test.test/john-d%C3%B6e-22219837', }, { title: 'should preserve already encoded special characters in path', - input: 'https://test.test/edouard-m%C3%A9nard-22219837', - expected: 'https://test.test/edouard-m%C3%A9nard-22219837', + input: 'https://test.test/john-d%C3%B6e-22219837', + expected: 'https://test.test/john-d%C3%B6e-22219837', }, { - title: 'should preserve special characters in query params', + title: 'should percent-encode non-ASCII characters in query params', input: 'https://example.com/path?name=José', - expected: 'https://example.com/path?name=José', + expected: 'https://example.com/path?name=Jos%C3%A9', }, { title: @@ -66,9 +66,9 @@ describe('normalizeUrlOrigin', () => { expected: 'https://example.com/test%2520name', }, { - title: 'should preserve special characters in hash fragments', + title: 'should percent-encode non-ASCII characters in hash fragments', input: 'https://example.com/path#frédéric', - expected: 'https://example.com/path#frédéric', + expected: 'https://example.com/path#fr%C3%A9d%C3%A9ric', }, { title: 'should keep encoded characters in hash fragments as-is', diff --git a/packages/twenty-shared/src/utils/url/normalizeUrlOrigin.ts b/packages/twenty-shared/src/utils/url/normalizeUrlOrigin.ts index 962573cd0c5..4a31c2ba67c 100644 --- a/packages/twenty-shared/src/utils/url/normalizeUrlOrigin.ts +++ b/packages/twenty-shared/src/utils/url/normalizeUrlOrigin.ts @@ -2,7 +2,8 @@ import { getURLSafely } from '@/utils/getURLSafely'; import { isDefined } from '@/utils/validation'; // Lowercases the URL origin (scheme + host) and removes a trailing slash. -// Preserves the raw path, query, and hash without decoding percent-encoded sequences. +// URL() already lowercases the origin and preserves percent-encoded sequences +// in the path, query, and hash (e.g. %2F stays %2F, %2520 stays %2520). export const normalizeUrlOrigin = (rawUrl: string) => { const url = getURLSafely(rawUrl); @@ -10,11 +11,8 @@ export const normalizeUrlOrigin = (rawUrl: string) => { return rawUrl; } - const lowercaseOrigin = url.origin.toLowerCase(); - const rawOrigin = rawUrl.match(/^[a-zA-Z][a-zA-Z\d+.-]*:\/\/[^/?#]+/)?.[0]; - const path = isDefined(rawOrigin) - ? rawUrl.slice(rawOrigin.length) - : url.pathname + url.search + url.hash; - - return (lowercaseOrigin + path).replace(/\/$/, ''); + return (url.origin + url.pathname + url.search + url.hash).replace( + /\/$/, + '', + ); };