Fix link formatting (#13210)
closes https://github.com/twentyhq/twenty/issues/13207 --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -79,11 +79,10 @@ describe('spreadsheetImportGetUnicityRowHook', () => {
|
||||
|
||||
it('should return row with error if row is not unique - index on composite field', () => {
|
||||
const hook = spreadsheetImportGetUnicityRowHook(mockObjectMetadataItem);
|
||||
|
||||
const testData: ImportedStructuredRow<string>[] = [
|
||||
{ 'Link URL (domainName)': 'duplicaTe.com' },
|
||||
{ 'Link URL (domainName)': 'duplicate.com ' },
|
||||
{ 'Link URL (domainName)': 'other.com' },
|
||||
{ 'Link URL (domainName)': 'https://duplicaTe.com' },
|
||||
{ 'Link URL (domainName)': 'https://duplicate.com' },
|
||||
{ 'Link URL (domainName)': 'https://other.com' },
|
||||
];
|
||||
|
||||
const addErrorMock = jest.fn();
|
||||
|
||||
@ -12,7 +12,7 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
isDefined,
|
||||
lowercaseUrlAndRemoveTrailingSlash,
|
||||
lowercaseUrlOriginAndRemoveTrailingSlash,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
type Column = {
|
||||
@ -108,7 +108,7 @@ const getUniqueValues = (
|
||||
.primaryLinkUrl,
|
||||
)
|
||||
) {
|
||||
return lowercaseUrlAndRemoveTrailingSlash(
|
||||
return lowercaseUrlOriginAndRemoveTrailingSlash(
|
||||
row?.[columnName]?.toString().trim() || '',
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import {
|
||||
isDefined,
|
||||
lowercaseUrlAndRemoveTrailingSlash,
|
||||
lowercaseUrlOriginAndRemoveTrailingSlash,
|
||||
parseJson,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { removeEmptyLinks } from 'src/engine/core-modules/record-transformer/utils/remove-empty-links';
|
||||
@ -16,10 +17,11 @@ export type LinksFieldGraphQLInput =
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
// TODO refactor this function handle partial composite field update
|
||||
export const transformLinksValue = (
|
||||
value: LinksFieldGraphQLInput,
|
||||
): LinksFieldGraphQLInput => {
|
||||
if (!value) {
|
||||
if (!isDefined(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@ -27,15 +29,9 @@ export const transformLinksValue = (
|
||||
const primaryLinkLabelRaw = value.primaryLinkLabel as string | null;
|
||||
const secondaryLinksRaw = value.secondaryLinks as string | null;
|
||||
|
||||
let secondaryLinksArray: LinkMetadataNullable[] | null = null;
|
||||
|
||||
if (isNonEmptyString(secondaryLinksRaw)) {
|
||||
try {
|
||||
secondaryLinksArray = JSON.parse(secondaryLinksRaw);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
const secondaryLinksArray = isNonEmptyString(secondaryLinksRaw)
|
||||
? parseJson<LinkMetadataNullable[]>(secondaryLinksRaw)
|
||||
: null;
|
||||
|
||||
const { primaryLinkLabel, primaryLinkUrl, secondaryLinks } = removeEmptyLinks(
|
||||
{
|
||||
@ -48,14 +44,14 @@ export const transformLinksValue = (
|
||||
return {
|
||||
...value,
|
||||
primaryLinkUrl: isDefined(primaryLinkUrl)
|
||||
? lowercaseUrlAndRemoveTrailingSlash(primaryLinkUrl)
|
||||
? lowercaseUrlOriginAndRemoveTrailingSlash(primaryLinkUrl)
|
||||
: primaryLinkUrl,
|
||||
primaryLinkLabel,
|
||||
secondaryLinks: JSON.stringify(
|
||||
secondaryLinks?.map((link) => ({
|
||||
...link,
|
||||
url: isDefined(link.url)
|
||||
? lowercaseUrlAndRemoveTrailingSlash(link.url)
|
||||
? lowercaseUrlOriginAndRemoveTrailingSlash(link.url)
|
||||
: link.url,
|
||||
})),
|
||||
),
|
||||
|
||||
@ -5,7 +5,7 @@ import axios, { AxiosInstance } from 'axios';
|
||||
import uniqBy from 'lodash.uniqby';
|
||||
import { TWENTY_COMPANIES_BASE_URL } from 'twenty-shared/constants';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { lowercaseUrlAndRemoveTrailingSlash } from 'twenty-shared/utils';
|
||||
import { lowercaseUrlOriginAndRemoveTrailingSlash } from 'twenty-shared/utils';
|
||||
import { DeepPartial, ILike, Repository } from 'typeorm';
|
||||
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
@ -79,7 +79,7 @@ export class CreateCompanyService {
|
||||
const companiesWithoutTrailingSlash = companies.map((company) => ({
|
||||
...company,
|
||||
domainName: company.domainName
|
||||
? lowercaseUrlAndRemoveTrailingSlash(company.domainName)
|
||||
? lowercaseUrlOriginAndRemoveTrailingSlash(company.domainName)
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { FieldMetadataType } from "@/types";
|
||||
import { FieldMetadataType } from '@/types';
|
||||
|
||||
export const LABEL_IDENTIFIER_FIELD_METADATA_TYPES = [
|
||||
FieldMetadataType.TEXT,
|
||||
|
||||
@ -105,11 +105,7 @@ describe('removeUndefinedFields', () => {
|
||||
},
|
||||
expected: {
|
||||
names: ['John', 'Jane', null],
|
||||
tags: [
|
||||
{ id: 1, label: 'active' },
|
||||
{ label: 'pending' },
|
||||
{ id: 3 },
|
||||
],
|
||||
tags: [{ id: 1, label: 'active' }, { label: 'pending' }, { id: 3 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -131,4 +127,4 @@ describe('removeUndefinedFields', () => {
|
||||
expect(removeUndefinedFields(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
7
packages/twenty-shared/src/utils/getURLSafely.ts
Normal file
7
packages/twenty-shared/src/utils/getURLSafely.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const getURLSafely = (url: string) => {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (e: unknown) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@ -9,6 +9,7 @@
|
||||
|
||||
export { assertUnreachable } from './assertUnreachable';
|
||||
export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind';
|
||||
export { getURLSafely } from './getURLSafely';
|
||||
export { getImageAbsoluteURI } from './image/getImageAbsoluteURI';
|
||||
export {
|
||||
sanitizeURL,
|
||||
@ -26,7 +27,7 @@ export { getAbsoluteUrlOrThrow } from './url/getAbsoluteUrlOrThrow';
|
||||
export { getUrlHostnameOrThrow } from './url/getUrlHostnameOrThrow';
|
||||
export { isValidHostname } from './url/isValidHostname';
|
||||
export { isValidUrl } from './url/isValidUrl';
|
||||
export { lowercaseUrlAndRemoveTrailingSlash } from './url/lowercaseUrlAndRemoveTrailingSlash';
|
||||
export { lowercaseUrlOriginAndRemoveTrailingSlash } from './url/lowercaseUrlOriginAndRemoveTrailingSlash';
|
||||
export { isDefined } from './validation/isDefined';
|
||||
export { isLabelIdentifierFieldMetadataTypes } from './validation/isLabelIdentifierFieldMetadataTypes';
|
||||
export { isValidLocale } from './validation/isValidLocale';
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
import { lowercaseUrlAndRemoveTrailingSlash } from '@/utils/url/lowercaseUrlAndRemoveTrailingSlash';
|
||||
|
||||
describe('lowercaseUrlAndRemoveTrailingSlash', () => {
|
||||
it('should leave lowcased domain unchanged', () => {
|
||||
const primaryLinkUrl = 'https://www.example.com/test';
|
||||
const result = lowercaseUrlAndRemoveTrailingSlash(primaryLinkUrl);
|
||||
|
||||
expect(result).toBe('https://www.example.com/test');
|
||||
});
|
||||
|
||||
it('should lowercase the domain of the primary link url', () => {
|
||||
const primaryLinkUrl = 'htTps://wwW.exAmple.coM/TEST';
|
||||
const result = lowercaseUrlAndRemoveTrailingSlash(primaryLinkUrl);
|
||||
|
||||
expect(result).toBe('https://www.example.com/TEST');
|
||||
});
|
||||
|
||||
it('should not add a trailing slash', () => {
|
||||
const primaryLinkUrl = 'https://www.example.com';
|
||||
const result = lowercaseUrlAndRemoveTrailingSlash(primaryLinkUrl);
|
||||
|
||||
expect(result).toBe('https://www.example.com');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,44 @@
|
||||
import { lowercaseUrlOriginAndRemoveTrailingSlash } from '@/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash';
|
||||
|
||||
interface TestContext {
|
||||
title: string;
|
||||
input: string;
|
||||
expected: string;
|
||||
}
|
||||
|
||||
describe('lowercaseUrlOriginAndRemoveTrailingSlash', () => {
|
||||
test.each<TestContext>([
|
||||
{
|
||||
title: 'should leave lowcased domain unchanged',
|
||||
input: 'https://www.example.com/test',
|
||||
expected: 'https://www.example.com/test',
|
||||
},
|
||||
{
|
||||
title: 'should lowercase the domain while preserving path case',
|
||||
input: 'htTps://wwW.exAmple.coM/TEST',
|
||||
expected: 'https://www.example.com/TEST',
|
||||
},
|
||||
{
|
||||
title: 'should not add a trailing slash',
|
||||
input: 'https://www.example.com',
|
||||
expected: 'https://www.example.com',
|
||||
},
|
||||
{
|
||||
title: 'should remove trailing slash',
|
||||
input: 'https://www.example.com/',
|
||||
expected: 'https://www.example.com',
|
||||
},
|
||||
{
|
||||
title: 'should handle query parameters',
|
||||
input: 'htTps://wwW.exAmple.coM/TEST?Param=Value',
|
||||
expected: 'https://www.example.com/TEST?Param=Value',
|
||||
},
|
||||
{
|
||||
title: 'should handle hash fragments',
|
||||
input: 'htTps://wwW.exAmple.coM/TEST#Hash',
|
||||
expected: 'https://www.example.com/TEST#Hash',
|
||||
},
|
||||
])('$title', ({ input, expected }) => {
|
||||
expect(lowercaseUrlOriginAndRemoveTrailingSlash(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@ -1,7 +0,0 @@
|
||||
export const lowercaseUrlAndRemoveTrailingSlash = (url: string) => {
|
||||
try {
|
||||
return new URL(url).toString().toLowerCase().replace(/\/$/, '');
|
||||
} catch {
|
||||
return url.toLowerCase();
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
import { getURLSafely } from '@/utils/getURLSafely';
|
||||
import { isDefined } from '@/utils/validation';
|
||||
|
||||
export const lowercaseUrlOriginAndRemoveTrailingSlash = (rawUrl: string) => {
|
||||
const url = getURLSafely(rawUrl);
|
||||
|
||||
if (!isDefined(url)) {
|
||||
return rawUrl;
|
||||
}
|
||||
|
||||
const lowercaseOrigin = url.origin.toLowerCase();
|
||||
const path = url.pathname + url.search + url.hash;
|
||||
|
||||
return (lowercaseOrigin + path).replace(/\/$/, '');
|
||||
};
|
||||
Reference in New Issue
Block a user