Skip to content

Commit

Permalink
Added failing test case
Browse files Browse the repository at this point in the history
  • Loading branch information
sagzy committed Sep 23, 2024
1 parent 7d441f2 commit 373347f
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 92 deletions.
92 changes: 1 addition & 91 deletions apps/admin-x-design-system/src/global/form/URLTextField.tsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,7 @@
import React, {useEffect, useState} from 'react';
import isEmail from 'validator/es/lib/isEmail';
import {useFocusContext} from '../../providers/DesignSystemProvider';
import TextField, {TextFieldProps} from './TextField';

export const formatUrl = (value: string, baseUrl?: string, nullable?: boolean) => {
if (nullable && !value) {
return {save: null, display: ''};
}

let url = value.trim();

if (!url) {
if (baseUrl) {
return {save: '/', display: baseUrl};
}
return {save: '', display: ''};
}

// if we have an email address, add the mailto:
if (isEmail(url)) {
return {save: `mailto:${url}`, display: `mailto:${url}`};
}

const isAnchorLink = url.match(/^#/);

if (isAnchorLink) {
return {save: url, display: url};
}

if (!baseUrl) {
// Absolute URL with no base URL
if (!url.startsWith('http')) {
url = `https://${url}`;
}
}

// If it doesn't look like a URL, leave it as is rather than assuming it's a pathname etc
if (!url.match(/^[a-zA-Z0-9-]+:/) && !url.match(/^(\/|\?)/)) {
return {save: url, display: url};
}

let parsedUrl: URL;

try {
parsedUrl = new URL(url, baseUrl);
} catch (e) {
return {save: url, display: url};
}

if (!baseUrl) {
return {save: parsedUrl.toString(), display: parsedUrl.toString()};
}
const parsedBaseUrl = new URL(baseUrl);

let isRelativeToBasePath = parsedUrl.pathname && parsedUrl.pathname.indexOf(parsedBaseUrl.pathname) === 0;

// if our path is only missing a trailing / mark it as relative
if (`${parsedUrl.pathname}/` === parsedBaseUrl.pathname) {
isRelativeToBasePath = true;
}

const isOnSameHost = parsedUrl.host === parsedBaseUrl.host;

// if relative to baseUrl, remove the base url before sending to action
if (!isAnchorLink && isOnSameHost && isRelativeToBasePath) {
url = url.replace(/^[a-zA-Z0-9-]+:/, '');
url = url.replace(/^\/\//, '');
url = url.replace(parsedBaseUrl.host, '');
url = url.replace(parsedBaseUrl.pathname, '');

// handle case where url path is same as baseUrl path but missing trailing slash
if (parsedUrl.pathname.slice(-1) !== '/') {
url = url.replace(parsedBaseUrl.pathname.slice(0, -1), '');
}

if (!url.match(/^\//)) {
url = `/${url}`;
}

if (!url.match(/\/$/) && !url.match(/[.#?]/)) {
url = `${url}/`;
}
}

if (url.match(/^(\/\/|#)/)) {
return {save: url, display: url};
}

// we update with the relative URL but then transform it back to absolute
// for the input value. This avoids problems where the underlying relative
// value hasn't changed even though the input value has
return {save: url, display: new URL(url, baseUrl).toString()};
};
import {formatUrl} from '../../utils/formatUrl';

export interface URLTextFieldProps extends Omit<TextFieldProps, 'value' | 'onChange'> {
baseUrl?: string;
Expand Down
3 changes: 2 additions & 1 deletion apps/admin-x-design-system/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export {default as Toggle} from './global/form/Toggle';
export type {ToggleProps} from './global/form/Toggle';
export {default as ToggleGroup} from './global/form/ToggleGroup';
export type {ToggleGroupProps} from './global/form/ToggleGroup';
export {default as URLTextField, formatUrl} from './global/form/URLTextField';
export {default as URLTextField} from './global/form/URLTextField';
export type {URLTextFieldProps} from './global/form/URLTextField';

export {default as ConfirmationModal, ConfirmationModalContent} from './global/modal/ConfirmationModal';
Expand Down Expand Up @@ -166,6 +166,7 @@ export {default as useSortableIndexedList} from './hooks/useSortableIndexedList'

export {debounce} from './utils/debounce';
export {confirmIfDirty} from './utils/modals';
export {formatUrl} from './utils/formatUrl';

export {default as DesignSystemApp} from './DesignSystemApp';
export type {DesignSystemAppProps} from './DesignSystemApp';
Expand Down
91 changes: 91 additions & 0 deletions apps/admin-x-design-system/src/utils/formatUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import isEmail from 'validator/es/lib/isEmail';

export const formatUrl = (value: string, baseUrl?: string, nullable?: boolean) => {
if (nullable && !value) {
return {save: null, display: ''};
}

let url = value.trim();

if (!url) {
if (baseUrl) {
return {save: '/', display: baseUrl};
}
return {save: '', display: ''};
}

// if we have an email address, add the mailto:
if (isEmail(url)) {
return {save: `mailto:${url}`, display: `mailto:${url}`};
}

const isAnchorLink = url.match(/^#/);
if (isAnchorLink) {
return {save: url, display: url};
}

const isProtocolRelative = url.match(/^(\/\/)/);
if (isProtocolRelative) {
return {save: url, display: url};
}

if (!baseUrl) {
// Absolute URL with no base URL
if (!url.startsWith('http')) {
url = `https://${url}`;
}
}

// If it doesn't look like a URL, leave it as is rather than assuming it's a pathname etc
if (!url.match(/^[a-zA-Z0-9-]+:/) && !url.match(/^(\/|\?)/)) {
return {save: url, display: url};
}

let parsedUrl: URL;

try {
parsedUrl = new URL(url, baseUrl);
} catch (e) {
return {save: url, display: url};
}

if (!baseUrl) {
return {save: parsedUrl.toString(), display: parsedUrl.toString()};
}
const parsedBaseUrl = new URL(baseUrl);

let isRelativeToBasePath = parsedUrl.pathname && parsedUrl.pathname.indexOf(parsedBaseUrl.pathname) === 0;

// if our path is only missing a trailing / mark it as relative
if (`${parsedUrl.pathname}/` === parsedBaseUrl.pathname) {
isRelativeToBasePath = true;
}

const isOnSameHost = parsedUrl.host === parsedBaseUrl.host;

// if relative to baseUrl, remove the base url before sending to action
if (isOnSameHost && isRelativeToBasePath) {
url = url.replace(/^[a-zA-Z0-9-]+:/, '');
url = url.replace(/^\/\//, '');
url = url.replace(parsedBaseUrl.host, '');
url = url.replace(parsedBaseUrl.pathname, '');

// handle case where url path is same as baseUrl path but missing trailing slash
if (parsedUrl.pathname.slice(-1) !== '/') {
url = url.replace(parsedBaseUrl.pathname.slice(0, -1), '');
}

if (!url.match(/^\//)) {
url = `/${url}`;
}

if (!url.match(/\/$/) && !url.match(/[.#?]/)) {
url = `${url}/`;
}
}

// we update with the relative URL but then transform it back to absolute
// for the input value. This avoids problems where the underlying relative
// value hasn't changed even though the input value has
return {save: url, display: new URL(url, baseUrl).toString()};
};
64 changes: 64 additions & 0 deletions apps/admin-x-design-system/test/unit/utils/formatUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as assert from 'assert/strict';
import {formatUrl} from '../../../src/utils/formatUrl';

describe.only('formatUrl', function () {
it('displays empty string if the input is empty and nullable is true', function () {
const formattedUrl = formatUrl('', undefined, true);
assert.deepEqual(formattedUrl, {save: null, display: ''});
});

it('displays empty string value if the input has only whitespace', function () {
const formattedUrl = formatUrl('');
assert.deepEqual(formattedUrl, {save: '', display: ''});
});

it('displays base value if the input has only whitespace and base url is available', function () {
const formattedUrl = formatUrl('', 'http://example.com');
assert.deepEqual(formattedUrl, {save: '/', display: 'http://example.com'});
});

it('displays a mailto address for an email address', function () {
const formattedUrl = formatUrl('test@example.com');
assert.deepEqual(formattedUrl, {save: 'mailto:test@example.com', display: 'mailto:test@example.com'});
});

it('displays an anchor link without formatting', function () {
const formattedUrl = formatUrl('#section');
assert.deepEqual(formattedUrl, {save: '#section', display: '#section'});
});

it('adds https:// automatically', function () {
const formattedUrl = formatUrl('example.com');
assert.deepEqual(formattedUrl, {save: 'https://example.com/', display: 'https://example.com/'});
});

it('adds a trailing slash / automatically', function () {
const formattedUrl = formatUrl('https://example.com');
assert.deepEqual(formattedUrl, {save: 'https://example.com/', display: 'https://example.com/'});
});

it('saves a relative URL is the input is a pathname', function () {
const formattedUrl = formatUrl('/path', 'http://example.com');
assert.deepEqual(formattedUrl, {save: '/path/', display: 'http://example.com/path/'});
});

it('saves a relative URL is the input is a pathname, even if the base url has an non-empty pathname', function () {
const formattedUrl = formatUrl('/path', 'http://example.com/blog');
assert.deepEqual(formattedUrl, {save: '/path/', display: 'http://example.com/blog/path/'});
});

it('saves a relative URL is the input includes the base url', function () {
const formattedUrl = formatUrl('http://example.com/path', 'http://example.com');
assert.deepEqual(formattedUrl, {save: '/path/', display: 'http://example.com/path/'});
});

it('saves an absolute URL is the input has a different pathname to the base url', function () {
const formattedUrl = formatUrl('http://example.com/path', 'http://example.com/blog');
assert.deepEqual(formattedUrl, {save: 'http://example.com/path', display: 'http://example.com/path'});
});

it('saves an absolte URL if the input has a different hostname to the base url', function () {
const formattedUrl = formatUrl('http://another.com/path', 'http://example.com');
assert.deepEqual(formattedUrl, {save: 'http://another.com/path', display: 'http://another.com/path'});
});
});

0 comments on commit 373347f

Please sign in to comment.