Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/cmsui/routes/auth/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ArrowRightSVG from '@plone/components/icons/arrow-right.svg?react';

import type PloneClient from '@plone/client';
import config from '@plone/registry';
import { UniversalLink } from '@plone/layout/components/UniversalLink/UniversalLink';

export const loader = redirectIfLoggedInLoader;

Expand Down Expand Up @@ -44,7 +45,8 @@ export default function Login() {
const actionResult = useActionData<typeof action>();

return (
<div className="mx-4 flex h-screen flex-1 flex-col justify-center">
<div>
<UniversalLink href="#ancor">test</UniversalLink>
<div className="flex flex-col items-center sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-quanta-sapphire flex h-32 w-32 flex-col items-center rounded-full p-8">
<img src={ploneSvg} alt="" />
Expand Down Expand Up @@ -85,6 +87,12 @@ export default function Login() {
>
<ArrowRightSVG />
</Button>
<h3
id="ancor"
style={{ paddingTop: '1000px', marginBottom: '1000px' }}
>
ancor
</h3>
</Form>
</div>
</div>
Expand Down
10 changes: 7 additions & 3 deletions packages/helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,20 @@
}
},
"dependencies": {
"@plone/react-router": "workspace:*",
"jotai": "^2.12.3",
"jotai-optics": "^0.4.0",
"optics-ts": "^2.4.1"
"optics-ts": "^2.4.1",
"react-router": "catalog:",
"validator": "^13.15.15"
},
"devDependencies": {
"@tanstack/react-form": "^1.3.3",
"@plone/types": "workspace:*",
"@plone/registry": "workspace:*",
"@plone/types": "workspace:*",
"@tanstack/react-form": "^1.3.3",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/validator": "^13.15.3",
"release-it": "catalog:",
"tsconfig": "workspace:*",
"tsup": "catalog:",
Expand Down
1 change: 1 addition & 0 deletions packages/helpers/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './primitives';
export * from './atoms';
export * from './flattenToAppURL';
export * from './urlUtils';
139 changes: 139 additions & 0 deletions packages/helpers/src/urlUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { matchPath } from 'react-router';
import config from '@plone/registry';

import validator from 'validator';

type ExternalRoute = string | { match: string };

interface Settings {
publicURL: string;
internalApiPath?: string;
apiPath: string;
externalRoutes?: ExternalRoute[];
}

export function isInternalURL(url: string): boolean {
const { settings } = config as { settings: Settings };

const isMatch = (settings.externalRoutes ?? []).find((route) => {
if (typeof route === 'object') {
return matchPath(url, route.match);
}
return matchPath(url, route);
});

const isExcluded = Boolean(isMatch && Object.keys(isMatch).length > 0);

const internalURL =
!!url &&
(url.indexOf(settings.publicURL) !== -1 ||
(settings.internalApiPath &&
url.indexOf(settings.internalApiPath) !== -1) ||
url.indexOf(settings.apiPath) !== -1 ||
url.charAt(0) === '/' ||
url.charAt(0) === '.' ||
url.startsWith('#'));

if (internalURL && isExcluded) {
return false;
}

return internalURL;
}

export function removeProtocol(
url: string,
protocol: string = 'https://',
): string {
return url
.replace(protocol, '')
.replace(protocol === 'https://' ? 'http://' : 'https://', '');
}

export function isMail(text: string): boolean {
return validator.isEmail(text);
}

export function isTelephone(text: string): boolean {
return validator.isMobilePhone(text, 'any'); // pode passar locale: 'it-IT', 'pt-BR', etc.
}

export function normaliseMail(email: string): string {
if (email?.toLowerCase()?.startsWith('mailto:')) {
return email;
}
return `mailto:${email}`;
}

export function normalizeTelephone(tel: string): string {
if (tel?.toLowerCase()?.startsWith('tel:')) {
return tel;
}
return `tel:${tel}`;
}

export function normalizeUrl(url: string): string {
if (!url) return '';

let candidate = url.trim();

// If the URL does not start with a protocol, add 'https://'
if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(candidate)) {
candidate = `https://${candidate}`;
}

// Validate the URL with validator.js
if (!validator.isURL(candidate, { require_protocol: true })) {
return ''; // Invalid URL, return empty string
}

// Use the URL constructor to normalize the URL
return new URL(candidate).href;
}

export function isUrl(url: string): boolean {
return validator.isURL(url, { require_protocol: true });
}

export function checkAndNormalizeUrl(url: string) {
const res = {
isMail: false,
isTelephone: false,
url: url,
isValid: true,
};

if (URLUtils.isMail(URLUtils.normaliseMail(url))) {
// Mail
res.isMail = true;
res.url = URLUtils.normaliseMail(url);
} else if (URLUtils.isTelephone(url)) {
// Phone
res.isTelephone = true;
res.url = URLUtils.normalizeTelephone(url);
} else {
// URL
if (
res.url?.length >= 0 &&
!res.url.startsWith('/') &&
!res.url.startsWith('#')
) {
res.url = URLUtils.normalizeUrl(url);
if (!URLUtils.isUrl(res.url)) {
res.isValid = false;
}
}
if (res.url === undefined || res.url === null) res.isValid = false;
}
return res;
}

export const URLUtils = {
normalizeTelephone,
normaliseMail,
normalizeUrl,
isTelephone,
isMail,
isUrl,
checkAndNormalizeUrl,
};
101 changes: 101 additions & 0 deletions packages/layout/components/UniversalLink/UniversalLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { forwardRef } from 'react';
import { Link as RouterLink } from '@plone/components';
import cx from 'clsx';
import { isInternalURL, URLUtils } from '@plone/helpers';

export type UniversalLinkProps = {
href?: string;
item?: {
'@id'?: string;
'@type'?: string;
remoteUrl?: string;
getRemoteUrl?: string;
};
openInNewTab?: boolean;
download?: boolean;
children: React.ReactNode;
className?: string;
title?: string;
smooth?: boolean;
token?: string; // prepared for future use, not used now
};

const getUrl = (href?: string, item?: UniversalLinkProps['item']): string => {
if (href) return href;
if (item) {
if (item.remoteUrl) return item.remoteUrl;
if (item.getRemoteUrl) return item.getRemoteUrl;
if (item['@id']) return item['@id'];
}
return '#';
};

export const UniversalLink = forwardRef<HTMLAnchorElement, UniversalLinkProps>(
(
{
href,
item,
children,
className,
title,
smooth,
openInNewTab,
download,
token,
},
ref,
) => {
const url = getUrl(href, item);

const checkedURL = URLUtils.checkAndNormalizeUrl(url);
const isExternal = !isInternalURL(url);
const isDownload = download || url.includes('@@download/file');
const isDisplayFile = url.includes('@@display-file/file');

if (isInternalURL(url)) {
if (isDownload) {
return (
<a href={url} ref={ref} className={className} title={title} download>
{children}
</a>
);
}
if (isDisplayFile) {
return (
<a
href={url}
ref={ref}
className={className}
title={title}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
);
}
return (
<RouterLink to={url} ref={ref} className={className} title={title}>
{children}
</RouterLink>
);
}

// external
return (
<a
href={url}
ref={ref}
className={cx('external', className)}
title={title}
target={openInNewTab ? '_blank' : undefined}
rel="noopener noreferrer"
download={isDownload ? true : undefined}
>
{children}
</a>
);
},
);

UniversalLink.displayName = 'UniversalLink';
1 change: 1 addition & 0 deletions packages/layout/news/7356.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Refactoring UniversalLink @Wagner3UB
1 change: 1 addition & 0 deletions packages/layout/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"dependencies": {
"@plone/blocks": "workspace:*",
"@plone/components": "workspace:*",
"@plone/helpers": "workspace:*",
"@plone/registry": "workspace:*",
"lodash.sortby": "^4.7.0",
"clsx": "^2.1.1",
Expand Down
21 changes: 20 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading