Fix
This commit is contained in:
-14
@@ -1,14 +0,0 @@
|
||||
import { normalizeUrl } from '@/navigation-menu-item/display/link/utils/normalizeUrl';
|
||||
|
||||
describe('normalizeUrl', () => {
|
||||
it('should leave url unchanged when it has protocol, otherwise prepend https', () => {
|
||||
expect(normalizeUrl('https://example.com')).toBe('https://example.com');
|
||||
expect(normalizeUrl('example.com')).toBe('https://example.com');
|
||||
expect(normalizeUrl(' example.com ')).toBe('https://example.com');
|
||||
});
|
||||
|
||||
it('should return empty string for empty or whitespace input', () => {
|
||||
expect(normalizeUrl('')).toBe('');
|
||||
expect(normalizeUrl(' ')).toBe('');
|
||||
});
|
||||
});
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
export const normalizeUrl = (url: string) => {
|
||||
const trimmedUrl = url.trim();
|
||||
|
||||
if (trimmedUrl === '') {
|
||||
return trimmedUrl;
|
||||
}
|
||||
|
||||
return trimmedUrl.startsWith('http://') || trimmedUrl.startsWith('https://')
|
||||
? trimmedUrl
|
||||
: `https://${trimmedUrl}`;
|
||||
};
|
||||
+1
-1
@@ -6,7 +6,7 @@ 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 '@/navigation-menu-item/display/link/utils/normalizeUrl';
|
||||
import { normalizeUrl } from 'twenty-shared/utils';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
export const useAddLinkToNavigationMenuDraft = () => {
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { getAbsoluteUrl } from 'twenty-shared/utils';
|
||||
import { ensureAbsoluteUrl } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { extractDomainFromUrl } from '@/navigation-menu-item/display/link/utils/extractDomainFromUrl';
|
||||
@@ -45,7 +45,7 @@ export const SidePanelEditLinkItemView = ({
|
||||
|
||||
const currentName = selectedItem.name ?? defaultLabel;
|
||||
const currentDomain = selectedItem.link
|
||||
? extractDomainFromUrl(getAbsoluteUrl(selectedItem.link))
|
||||
? extractDomainFromUrl(ensureAbsoluteUrl(selectedItem.link))
|
||||
: undefined;
|
||||
const canAutoUpdateName =
|
||||
currentName === defaultLabel ||
|
||||
@@ -57,7 +57,7 @@ export const SidePanelEditLinkItemView = ({
|
||||
if (!canAutoUpdateName) return;
|
||||
const trimmed = value.trim();
|
||||
if (!isNonEmptyString(trimmed)) return;
|
||||
const domain = extractDomainFromUrl(getAbsoluteUrl(trimmed));
|
||||
const domain = extractDomainFromUrl(ensureAbsoluteUrl(trimmed));
|
||||
if (domain !== undefined) {
|
||||
setLastAutoSetName(domain);
|
||||
onUpdateLink(selectedItem.id, { name: domain });
|
||||
@@ -67,7 +67,7 @@ export const SidePanelEditLinkItemView = ({
|
||||
const handleUrlBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value.trim();
|
||||
if (isNonEmptyString(value)) {
|
||||
onUpdateLink(selectedItem.id, { link: getAbsoluteUrl(value) });
|
||||
onUpdateLink(selectedItem.id, { link: ensureAbsoluteUrl(value) });
|
||||
setUrlEditInput('');
|
||||
}
|
||||
};
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecor
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useContext } from 'react';
|
||||
import { FieldMetadataSettingsOnClickAction } from 'twenty-shared/types';
|
||||
import { getAbsoluteUrl, isDefined } from 'twenty-shared/utils';
|
||||
import { ensureAbsoluteUrl, isDefined } from 'twenty-shared/utils';
|
||||
import { IconArrowUpRight, IconCopy } from 'twenty-ui/display';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
@@ -69,7 +69,7 @@ export const useGetSecondaryRecordTableCellButton = () => {
|
||||
if (isFieldLinks(fieldDefinition)) {
|
||||
const url = (fieldValue as FieldLinksValue).primaryLinkUrl ?? '';
|
||||
openLinkOnClick = () => {
|
||||
window.open(getAbsoluteUrl(url), '_blank');
|
||||
window.open(ensureAbsoluteUrl(url), '_blank');
|
||||
};
|
||||
copyOnClick = () => {
|
||||
copyToClipboard(url, t`Link copied to clipboard`);
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ import {
|
||||
assertUnreachable,
|
||||
isDefined,
|
||||
isEmptyObject,
|
||||
lowercaseUrlOriginAndRemoveTrailingSlash,
|
||||
normalizeUrlOrigin,
|
||||
} from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
@@ -185,7 +185,7 @@ export const buildRecordFromImportedStructuredRow = ({
|
||||
},
|
||||
[FieldMetadataType.LINKS]: {
|
||||
primaryLinkLabel: castToString,
|
||||
primaryLinkUrl: lowercaseUrlOriginAndRemoveTrailingSlash,
|
||||
primaryLinkUrl: normalizeUrlOrigin,
|
||||
secondaryLinks: linkArrayJSONSchema.parse,
|
||||
},
|
||||
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
getUniqueConstraintsFields,
|
||||
isDefined,
|
||||
lowercaseUrlOriginAndRemoveTrailingSlash,
|
||||
normalizeUrlOrigin,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
type Column = {
|
||||
@@ -104,7 +104,7 @@ const getUniqueValues = (
|
||||
.primaryLinkUrl,
|
||||
)
|
||||
) {
|
||||
return lowercaseUrlOriginAndRemoveTrailingSlash(
|
||||
return normalizeUrlOrigin(
|
||||
row?.[columnName]?.toString().trim() || '',
|
||||
);
|
||||
}
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@ import isEmpty from 'lodash.isempty';
|
||||
import { type LinkMetadataNullable } from 'twenty-shared/types';
|
||||
import {
|
||||
isDefined,
|
||||
lowercaseUrlOriginAndRemoveTrailingSlash,
|
||||
normalizeUrlOrigin,
|
||||
parseJson,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
@@ -45,14 +45,14 @@ export const transformLinksValue = (
|
||||
const processedSecondaryLinks = secondaryLinks?.map((link) => ({
|
||||
...link,
|
||||
url: isDefined(link.url)
|
||||
? lowercaseUrlOriginAndRemoveTrailingSlash(link.url)
|
||||
? normalizeUrlOrigin(link.url)
|
||||
: link.url,
|
||||
}));
|
||||
|
||||
return {
|
||||
...value,
|
||||
primaryLinkUrl: isDefined(primaryLinkUrl)
|
||||
? lowercaseUrlOriginAndRemoveTrailingSlash(primaryLinkUrl)
|
||||
? normalizeUrlOrigin(primaryLinkUrl)
|
||||
: primaryLinkUrl,
|
||||
primaryLinkLabel,
|
||||
secondaryLinks: isEmpty(processedSecondaryLinks)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { getAbsoluteUrl } from 'twenty-shared/utils';
|
||||
import { ensureAbsoluteUrl } from 'twenty-shared/utils';
|
||||
|
||||
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||
import { WEBHOOK_RESPONSE_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/webhook/webhook-response';
|
||||
@@ -79,7 +79,7 @@ export class CallWebhookJob {
|
||||
);
|
||||
|
||||
const response = await axiosClient.post(
|
||||
getAbsoluteUrl(data.targetUrl),
|
||||
ensureAbsoluteUrl(data.targetUrl),
|
||||
payloadWithoutSecret,
|
||||
{
|
||||
headers,
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ import {
|
||||
} from 'twenty-shared/types';
|
||||
import {
|
||||
isDefined,
|
||||
lowercaseUrlOriginAndRemoveTrailingSlash,
|
||||
normalizeUrlOrigin,
|
||||
} from 'twenty-shared/utils';
|
||||
import { type DeepPartial, ILike } from 'typeorm';
|
||||
|
||||
@@ -71,7 +71,7 @@ export class CreateCompanyService {
|
||||
const companiesWithoutTrailingSlash = companies.map((company) => ({
|
||||
...company,
|
||||
domainName: company.domainName
|
||||
? lowercaseUrlOriginAndRemoveTrailingSlash(company.domainName)
|
||||
? normalizeUrlOrigin(company.domainName)
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
|
||||
@@ -180,12 +180,13 @@ export { isRecordGqlOperationSignature } from './typeguard/isRecordGqlOperationS
|
||||
export { throwIfNotDefined } from './typeguard/throwIfNotDefined';
|
||||
export { absoluteUrlSchema } from './url/absoluteUrlSchema';
|
||||
export { buildSignedPath } from './url/buildSignedPath';
|
||||
export { getAbsoluteUrl } from './url/getAbsoluteUrl';
|
||||
export { ensureAbsoluteUrl } from './url/ensureAbsoluteUrl';
|
||||
export { getAbsoluteUrlOrThrow } from './url/getAbsoluteUrlOrThrow';
|
||||
export { getUrlHostnameOrThrow } from './url/getUrlHostnameOrThrow';
|
||||
export { isValidHostname } from './url/isValidHostname';
|
||||
export { isValidUrl } from './url/isValidUrl';
|
||||
export { lowercaseUrlOriginAndRemoveTrailingSlash } from './url/lowercaseUrlOriginAndRemoveTrailingSlash';
|
||||
export { normalizeUrl } from './url/normalizeUrl';
|
||||
export { normalizeUrlOrigin } from './url/normalizeUrlOrigin';
|
||||
export { safeDecodeURIComponent } from './url/safeDecodeURIComponent';
|
||||
export { uuidToBase36 } from './uuidToBase36';
|
||||
export { assertIsDefinedOrThrow } from './validation/assertIsDefinedOrThrow';
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ensureAbsoluteUrl } from '@/utils/url/ensureAbsoluteUrl';
|
||||
|
||||
describe('ensureAbsoluteUrl', () => {
|
||||
it('should return https URL as-is (trimmed)', () => {
|
||||
expect(ensureAbsoluteUrl('https://example.com')).toBe(
|
||||
'https://example.com',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return http URL as-is', () => {
|
||||
expect(ensureAbsoluteUrl('http://example.com')).toBe('http://example.com');
|
||||
});
|
||||
|
||||
it('should return HTTPS URL as-is', () => {
|
||||
expect(ensureAbsoluteUrl('HTTPS://example.com')).toBe(
|
||||
'HTTPS://example.com',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return HTTP URL as-is', () => {
|
||||
expect(ensureAbsoluteUrl('HTTP://example.com')).toBe('HTTP://example.com');
|
||||
});
|
||||
|
||||
it('should prepend https:// to bare domains', () => {
|
||||
expect(ensureAbsoluteUrl('example.com')).toBe('https://example.com');
|
||||
});
|
||||
|
||||
it('should trim whitespace before processing', () => {
|
||||
expect(ensureAbsoluteUrl(' example.com ')).toBe('https://example.com');
|
||||
expect(ensureAbsoluteUrl(' https://example.com ')).toBe(
|
||||
'https://example.com',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import { getAbsoluteUrl } from '@/utils/url/getAbsoluteUrl';
|
||||
|
||||
describe('getAbsoluteUrl', () => {
|
||||
it('should return https URL as-is', () => {
|
||||
expect(getAbsoluteUrl('https://example.com')).toBe('https://example.com');
|
||||
});
|
||||
|
||||
it('should return http URL as-is', () => {
|
||||
expect(getAbsoluteUrl('http://example.com')).toBe('http://example.com');
|
||||
});
|
||||
|
||||
it('should return HTTPS URL as-is', () => {
|
||||
expect(getAbsoluteUrl('HTTPS://example.com')).toBe('HTTPS://example.com');
|
||||
});
|
||||
|
||||
it('should return HTTP URL as-is', () => {
|
||||
expect(getAbsoluteUrl('HTTP://example.com')).toBe('HTTP://example.com');
|
||||
});
|
||||
|
||||
it('should prepend https:// to bare domains', () => {
|
||||
expect(getAbsoluteUrl('example.com')).toBe('https://example.com');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { normalizeUrl } from '@/utils/url/normalizeUrl';
|
||||
|
||||
describe('normalizeUrl', () => {
|
||||
it('should return empty string for empty or whitespace input', () => {
|
||||
expect(normalizeUrl('')).toBe('');
|
||||
expect(normalizeUrl(' ')).toBe('');
|
||||
});
|
||||
|
||||
it('should prepend https and normalize origin for bare domains', () => {
|
||||
expect(normalizeUrl('example.com')).toBe('https://example.com');
|
||||
expect(normalizeUrl(' example.com ')).toBe('https://example.com');
|
||||
});
|
||||
|
||||
it('should lowercase the origin and preserve the path', () => {
|
||||
expect(normalizeUrl('HTTPS://WWW.Example.COM/Path')).toBe(
|
||||
'https://www.example.com/Path',
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove trailing slash', () => {
|
||||
expect(normalizeUrl('https://example.com/')).toBe('https://example.com');
|
||||
});
|
||||
|
||||
it('should preserve percent-encoded sequences', () => {
|
||||
expect(normalizeUrl('https://example.com/path%2Fencoded')).toBe(
|
||||
'https://example.com/path%2Fencoded',
|
||||
);
|
||||
});
|
||||
});
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { lowercaseUrlOriginAndRemoveTrailingSlash } from '@/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash';
|
||||
import { normalizeUrlOrigin } from '@/utils/url/normalizeUrlOrigin';
|
||||
|
||||
interface TestContext {
|
||||
title: string;
|
||||
@@ -6,7 +6,7 @@ interface TestContext {
|
||||
expected: string;
|
||||
}
|
||||
|
||||
describe('lowercaseUrlOriginAndRemoveTrailingSlash', () => {
|
||||
describe('normalizeUrlOrigin', () => {
|
||||
test.each<TestContext>([
|
||||
{
|
||||
title: 'should leave lowcased domain unchanged',
|
||||
@@ -88,6 +88,6 @@ describe('lowercaseUrlOriginAndRemoveTrailingSlash', () => {
|
||||
'https://www.google.com/maps/place/Birdie+-+Eventlocation/data=!4m7!3m6!1s0x479e7674e1702985:0xe482992505cb1ba4!8m2!3d48.1584971!4d11.5538261!16s%2Fg%2F1ptwh8096!19sChIJhSlw4XR2nkcRpBvLBSWZguQ?authuser=0&hl=en&rclk=1',
|
||||
},
|
||||
])('$title', ({ input, expected }) => {
|
||||
expect(lowercaseUrlOriginAndRemoveTrailingSlash(input)).toBe(expected);
|
||||
expect(normalizeUrlOrigin(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import { getAbsoluteUrl } from '@/utils/url/getAbsoluteUrl';
|
||||
import { ensureAbsoluteUrl } from '@/utils/url/ensureAbsoluteUrl';
|
||||
import { isValidHostname } from '@/utils/url/isValidHostname';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const absoluteUrlSchema = z.string().transform((value, ctx) => {
|
||||
const trimmedValue = value.trim();
|
||||
const absoluteUrl = getAbsoluteUrl(trimmedValue);
|
||||
const absoluteUrl = ensureAbsoluteUrl(trimmedValue);
|
||||
|
||||
const valueWithoutProtocol = absoluteUrl
|
||||
.replace('https://', '')
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export const ensureAbsoluteUrl = (value: string): string => {
|
||||
const trimmedValue = value.trim();
|
||||
|
||||
if (
|
||||
trimmedValue.startsWith('http://') ||
|
||||
trimmedValue.startsWith('https://') ||
|
||||
trimmedValue.startsWith('HTTPS://') ||
|
||||
trimmedValue.startsWith('HTTP://')
|
||||
) {
|
||||
return trimmedValue;
|
||||
}
|
||||
|
||||
return `https://${trimmedValue}`;
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
export const getAbsoluteUrl = (value: string): string => {
|
||||
if (
|
||||
value.startsWith('http://') ||
|
||||
value.startsWith('https://') ||
|
||||
value.startsWith('HTTPS://') ||
|
||||
value.startsWith('HTTP://')
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return `https://${value}`;
|
||||
};
|
||||
@@ -1,7 +1,10 @@
|
||||
export * from './absoluteUrlSchema';
|
||||
export * from './ensureAbsoluteUrl';
|
||||
export * from './getAbsoluteUrlOrThrow';
|
||||
export * from './getUrlHostnameOrThrow';
|
||||
export * from './isValidHostname';
|
||||
export * from './isValidUrl';
|
||||
export * from './normalizeUrl';
|
||||
export * from './normalizeUrlOrigin';
|
||||
export * from './buildSignedPath';
|
||||
export * from './safeDecodeURIComponent';
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ensureAbsoluteUrl } from '@/utils/url/ensureAbsoluteUrl';
|
||||
import { normalizeUrlOrigin } from '@/utils/url/normalizeUrlOrigin';
|
||||
|
||||
// Ensures the URL has a protocol, lowercases the origin, and removes a trailing slash.
|
||||
export const normalizeUrl = (url: string): string => {
|
||||
const trimmed = url.trim();
|
||||
|
||||
if (trimmed === '') {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
return normalizeUrlOrigin(ensureAbsoluteUrl(trimmed));
|
||||
};
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
import { getURLSafely } from '@/utils/getURLSafely';
|
||||
import { isDefined } from '@/utils/validation';
|
||||
|
||||
export const lowercaseUrlOriginAndRemoveTrailingSlash = (rawUrl: string) => {
|
||||
// Lowercases the URL origin (scheme + host) and removes a trailing slash.
|
||||
// Preserves the raw path, query, and hash without decoding percent-encoded sequences.
|
||||
export const normalizeUrlOrigin = (rawUrl: string) => {
|
||||
const url = getURLSafely(rawUrl);
|
||||
|
||||
if (!isDefined(url)) {
|
||||
Reference in New Issue
Block a user