mirror of
https://github.com/inventree/InvenTree.git
synced 2026-11-10 02:47:49 +00:00
[UI] Add link rendering helper for plugin (#13193)
- Adds reusable helpers for rendering sanitized links - Can be used by plugins
This commit is contained in:
1 parent
9416b81799
commit
5fe105b948
4 files changed
+116
-39
No files matched your search
@@ -2,6 +2,13 @@
|
||||
|
||||
This file contains historical changelog information for the InvenTree UI components library.
|
||||
|
||||
### 1.8.0 - Unreleased
|
||||
|
||||
#### New Features
|
||||
|
||||
- Adds `renderLink` helper function, which renders a validated external URL (`http` / `https` only) as an `<Anchor>` element with a link icon
|
||||
- Adds `sanitizeLinkUrl` helper function, which validates a URL for use as an external link
|
||||
|
||||
### 1.7.0 - November 2026
|
||||
|
||||
Updated UI components library to match 1.7.0 InvenTree release.
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Anchor, type AnchorProps } from '@mantine/core';
|
||||
import { IconLink } from '@tabler/icons-react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { cancelEvent } from './Events';
|
||||
|
||||
// Only these URL protocols are permitted for rendered links
|
||||
const ALLOWED_LINK_PROTOCOLS: string[] = ['http:', 'https:'];
|
||||
|
||||
/**
|
||||
* Validate a user-supplied URL for use as an external link.
|
||||
*
|
||||
* - The URL must be absolute and parseable
|
||||
* - Only http and https protocols are permitted
|
||||
* (this rejects e.g. javascript:, data:, vbscript:, file: URLs)
|
||||
*
|
||||
* @param url - The URL to validate
|
||||
* @returns The normalized URL string if valid, otherwise null
|
||||
*/
|
||||
export function sanitizeLinkUrl(url: any): string | null {
|
||||
if (typeof url !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = url.trim();
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
|
||||
if (!ALLOWED_LINK_PROTOCOLS.includes(parsed.protocol.toLowerCase())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsed.href;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export type RenderLinkOptions = {
|
||||
/** Icon size (default = 18) */
|
||||
size?: number;
|
||||
/** Optional content to render instead of the link icon */
|
||||
children?: ReactNode;
|
||||
/** Content to render if the URL is empty (default = '-') */
|
||||
placeholder?: ReactNode;
|
||||
/** Additional props passed through to the Anchor component */
|
||||
anchorProps?: AnchorProps;
|
||||
};
|
||||
|
||||
/**
|
||||
* Render an external link as an <Anchor> element with a link icon.
|
||||
*
|
||||
* The URL is validated before rendering - only absolute http / https URLs
|
||||
* are rendered as clickable links. Invalid or unsafe URLs are rendered
|
||||
* as plain (escaped) text, so that the value is still visible to the user.
|
||||
*
|
||||
* Links are opened in a new tab, with "noopener" and "noreferrer" set.
|
||||
*
|
||||
* @param url - The URL to render
|
||||
* @param options - Optional rendering options
|
||||
*/
|
||||
export function renderLink(
|
||||
url: string | null | undefined,
|
||||
options: RenderLinkOptions = {}
|
||||
): ReactNode {
|
||||
const placeholder = options.placeholder ?? '-';
|
||||
|
||||
if (!url || (typeof url === 'string' && !url.trim())) {
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
const href = sanitizeLinkUrl(url);
|
||||
|
||||
if (!href) {
|
||||
// Not a valid / permitted URL - render as plain text (React escapes this)
|
||||
return String(url);
|
||||
}
|
||||
|
||||
return (
|
||||
<Anchor
|
||||
{...options.anchorProps}
|
||||
href={href}
|
||||
target='_blank'
|
||||
rel='noreferrer noopener'
|
||||
title={href}
|
||||
onClick={(event: any) => {
|
||||
cancelEvent(event);
|
||||
window.open(href, '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
>
|
||||
{options.children ?? <IconLink size={options.size ?? 18} />}
|
||||
</Anchor>
|
||||
);
|
||||
}
|
||||
@@ -75,6 +75,12 @@ export {
|
||||
navigateToLink
|
||||
} from './functions/Navigation';
|
||||
|
||||
export {
|
||||
renderLink,
|
||||
sanitizeLinkUrl,
|
||||
type RenderLinkOptions
|
||||
} from './functions/Links';
|
||||
|
||||
export {
|
||||
permissionDenied,
|
||||
invalidResponse,
|
||||
|
||||
@@ -2,27 +2,14 @@
|
||||
* Common rendering functions for table column data.
|
||||
*/
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
Anchor,
|
||||
Badge,
|
||||
Center,
|
||||
Group,
|
||||
Skeleton,
|
||||
Text,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconBell,
|
||||
IconExclamationCircle,
|
||||
IconLink,
|
||||
IconLock
|
||||
} from '@tabler/icons-react';
|
||||
import { Badge, Center, Group, Skeleton, Text, Tooltip } from '@mantine/core';
|
||||
import { IconBell, IconExclamationCircle, IconLock } from '@tabler/icons-react';
|
||||
|
||||
import { ProgressBar } from '@lib/components/ProgressBar';
|
||||
import { YesNoButton } from '@lib/components/YesNoButton';
|
||||
import type { ModelType } from '@lib/enums/ModelType';
|
||||
import { resolveItem } from '@lib/functions/Conversion';
|
||||
import { cancelEvent } from '@lib/functions/Events';
|
||||
import { renderLink } from '@lib/functions/Links';
|
||||
import type { TableColumn, TableColumnProps } from '@lib/types/Tables';
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
@@ -508,29 +495,8 @@ export function LinkColumn(props: TableColumnProps): TableColumn {
|
||||
defaultVisible: false,
|
||||
copyable: true,
|
||||
copyAccessor: props.accessor ?? 'link',
|
||||
render: (record: any) => {
|
||||
const url = resolveItem(record, props.accessor ?? 'link');
|
||||
|
||||
if (!url) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return (
|
||||
<Anchor
|
||||
href={url}
|
||||
target='_blank'
|
||||
rel='noreferrer noopener'
|
||||
onClick={(event: any) => {
|
||||
cancelEvent(event);
|
||||
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
title={url}
|
||||
>
|
||||
<IconLink size={18} />
|
||||
</Anchor>
|
||||
);
|
||||
},
|
||||
render: (record: any) =>
|
||||
renderLink(resolveItem(record, props.accessor ?? 'link')),
|
||||
...props
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user