2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-15 07:48:51 +00:00

[plugin] Render tables (#11733)

* Move useFilterSet state to the @lib

* Refactor useTable hook into @lib

* Refactor string helper functions

* Refactor constructFormUrl func

* Refactor Boundary component

* Refactor StoredTableState

* More refactoring

* Refactor CopyButton and CopyableCell

* Pass table render func to plugins

* Provide internal wrapper function, while allowing the "api" and "navigate" functions to be provided by the caller

* Adds <InvenTreeTable /> component which is exposed to plugins

* Update frontend versioning

* Update docs

* Handle condition where UI does not provide table rendering function

* Move queryFilters out of custom state

* Fix exported type

* Extract searchParams

- Cannot be used outside of router component
- Only provide when the table is generated internally

* Bump UI version

* Fix for right-click context menu

- Function needs to be defined with the context menu provider
This commit is contained in:
Oliver
2026-04-13 20:36:29 +10:00
committed by GitHub
parent 27ce60dea3
commit 23f43ffd33
120 changed files with 506 additions and 304 deletions

View File

@@ -0,0 +1,46 @@
import { t } from '@lingui/core/macro';
import { Alert } from '@mantine/core';
import { ErrorBoundary, type FallbackRender } from '@sentry/react';
import { IconExclamationCircle } from '@tabler/icons-react';
import { type ReactNode, useCallback } from 'react';
export function DefaultFallback({
title
}: Readonly<{ title: string }>): ReactNode {
return (
<Alert
color='red'
icon={<IconExclamationCircle />}
title={`${t`Error rendering component`}: ${title}`}
>
{t`An error occurred while rendering this component. Refer to the console for more information.`}
</Alert>
);
}
export function Boundary({
children,
label,
fallback
}: Readonly<{
children: ReactNode;
label: string;
fallback?: React.ReactElement<any> | FallbackRender;
}>): ReactNode {
const onError = useCallback(
(error: unknown, componentStack: string | undefined, eventId: string) => {
console.error(`ERR: Error rendering component: ${label}`);
console.error(error);
},
[]
);
return (
<ErrorBoundary
fallback={fallback ?? <DefaultFallback title={label} />}
onError={onError}
>
{children}
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,76 @@
import { t } from '@lingui/core/macro';
import {
ActionIcon,
type ActionIconVariant,
Button,
type DefaultMantineColor,
type FloatingPosition,
CopyButton as MantineCopyButton,
type MantineSize,
Text,
Tooltip
} from '@mantine/core';
import { IconCheck, IconCopy } from '@tabler/icons-react';
import type { JSX } from 'react';
export function CopyButton({
value,
label,
tooltip,
disabled,
tooltipPosition,
content,
size,
color = 'gray',
variant = 'transparent'
}: Readonly<{
value: any;
label?: string;
tooltip?: string;
disabled?: boolean;
tooltipPosition?: FloatingPosition;
content?: JSX.Element;
size?: MantineSize;
color?: DefaultMantineColor;
variant?: ActionIconVariant;
}>) {
const ButtonComponent = label ? Button : ActionIcon;
// Disable the copy button if we are not in a secure context, as the Clipboard API is not available
if (!window.isSecureContext) {
return null;
}
return (
<MantineCopyButton value={value}>
{({ copied, copy }) => (
<Tooltip
label={copied ? t`Copied` : (tooltip ?? t`Copy`)}
withArrow
position={tooltipPosition}
>
<ButtonComponent
disabled={disabled}
color={copied ? 'teal' : color}
onClick={(e: any) => {
e.stopPropagation();
e.preventDefault();
copy();
}}
variant={copied ? 'transparent' : (variant ?? 'transparent')}
size={size ?? 'sm'}
>
{copied ? <IconCheck /> : <IconCopy />}
{content}
{label && (
<Text p={size ?? 'sm'} size={size ?? 'sm'}>
{label}
</Text>
)}
</ButtonComponent>
</Tooltip>
)}
</MantineCopyButton>
);
}

View File

@@ -0,0 +1,51 @@
import { Group } from '@mantine/core';
import { useState } from 'react';
import { CopyButton } from './CopyButton';
/**
* A wrapper component that adds a copy button to cell content on hover
* This component is used to make table cells copyable without adding visual clutter
*
* @param children - The cell content to render
* @param value - The value to copy when the copy button is clicked
*/
export function CopyableCell({
children,
value
}: Readonly<{
children: React.ReactNode;
value: string;
}>) {
const [isHovered, setIsHovered] = useState(false);
return (
<Group
gap={0}
p={0}
wrap='nowrap'
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
justify='space-between'
align='center'
>
{children}
{window.isSecureContext && isHovered && value != null && (
<span
style={{ position: 'relative' }}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<div
style={{
position: 'absolute',
right: 0,
transform: 'translateY(-50%)'
}}
>
<CopyButton value={value} variant={'default'} />
</div>
</span>
)}
</Group>
);
}

View File

@@ -0,0 +1,63 @@
import { Alert } from '@mantine/core';
import {
INVENTREE_PLUGIN_VERSION,
type InvenTreePluginContext
} from '../types/Plugins';
import type {
InvenTreeTableProps,
TableColumn,
TableState
} from '../types/Tables';
/**
* Wrapper function which allows plugins to render an InvenTree component instance directly,
* in a similar way to the standard InvenTreeTable component.
*
* Note: The InventreePluginContext "context" object must be provided when rendering the table
*
*/
export default function InvenTreeTable({
url,
tableState,
tableData,
columns,
props,
context
}: {
url?: string;
tableState: TableState;
tableData?: any[];
columns: TableColumn<any>[];
props: InvenTreeTableProps;
context: InvenTreePluginContext;
}) {
if (!context?.tables?.renderTable) {
return (
<Alert title='Plugin Version Error' color='red'>
{
'The <InvenTreeTable> component cannot be rendered because the plugin context is missing the "renderTable" function.'
}
<br />
{
'This means that the InvenTree UI library version is incompatible with this plugin version.'
}
<br />
<b>Plugin Version:</b> {INVENTREE_PLUGIN_VERSION}
<br />
<b>UI Version:</b> {context?.version?.inventree || 'unknown'}
<br />
</Alert>
);
}
return context?.tables.renderTable({
url: url,
tableState: tableState,
tableData: tableData,
columns: columns,
props: props,
api: context.api,
navigate: context.navigate
});
}

View File

@@ -0,0 +1,40 @@
import { t } from '@lingui/core/macro';
import { ActionIcon, Checkbox, Divider, Menu, Tooltip } from '@mantine/core';
import { IconAdjustments } from '@tabler/icons-react';
export function TableColumnSelect({
columns,
onToggleColumn
}: Readonly<{
columns: any[];
onToggleColumn: (columnName: string) => void;
}>) {
return (
<Menu shadow='xs' closeOnItemClick={false}>
<Menu.Target>
<ActionIcon variant='transparent' aria-label='table-select-columns'>
<Tooltip label={t`Select Columns`} position='top-end'>
<IconAdjustments />
</Tooltip>
</ActionIcon>
</Menu.Target>
<Menu.Dropdown style={{ maxHeight: '400px', overflowY: 'auto' }}>
<Menu.Label>{t`Select Columns`}</Menu.Label>
<Divider />
{columns
.filter((col) => col.switchable ?? true)
.map((col) => (
<Menu.Item key={col.accessor}>
<Checkbox
checked={!col.hidden}
label={col.title || col.accessor}
onChange={() => onToggleColumn(col.accessor)}
radius='sm'
/>
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
);
}