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:
46
src/frontend/lib/components/Boundary.tsx
Normal file
46
src/frontend/lib/components/Boundary.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
76
src/frontend/lib/components/CopyButton.tsx
Normal file
76
src/frontend/lib/components/CopyButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
51
src/frontend/lib/components/CopyableCell.tsx
Normal file
51
src/frontend/lib/components/CopyableCell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
63
src/frontend/lib/components/InvenTreeTable.tsx
Normal file
63
src/frontend/lib/components/InvenTreeTable.tsx
Normal 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
|
||||
});
|
||||
}
|
||||
40
src/frontend/lib/components/TableColumnSelect.tsx
Normal file
40
src/frontend/lib/components/TableColumnSelect.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user