[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
+46
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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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
});
}
@@ -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>
);
}
+47
View File
@@ -0,0 +1,47 @@
import type { ApiEndpoints } from '../enums/ApiEndpoints';
import type { PathParams } from '../types/Core';
import type { ApiFormFieldSet, ApiFormFieldType } from '../types/Forms';
import { apiUrl } from './Api';
/**
* Construct an API url from the provided ApiFormProps object
*/
export function constructFormUrl(
url: ApiEndpoints | string,
pk?: string | number,
pathParams?: PathParams,
queryParams?: URLSearchParams
): string {
let formUrl = apiUrl(url, pk, pathParams);
if (queryParams) {
formUrl += `?${queryParams.toString()}`;
}
return formUrl;
}
export type NestedDict = { [key: string]: string | number | NestedDict };
export function mapFields(
fields: ApiFormFieldSet,
fieldFunction: (path: string, value: ApiFormFieldType, key: string) => any,
_path?: string
): NestedDict {
const res: NestedDict = {};
for (const [k, v] of Object.entries(fields)) {
const path = _path ? `${_path}.${k}` : k;
let value: any;
if (v.field_type === 'nested object' && v.children) {
value = mapFields(v.children, fieldFunction, path);
} else {
value = fieldFunction(path, v, k);
}
if (value !== undefined) res[k] = value;
}
return res;
}
@@ -0,0 +1,50 @@
import { t } from '@lingui/core/macro';
import { notifications } from '@mantine/notifications';
/**
* Show a notification that the feature is not yet implemented
*/
export function notYetImplemented() {
notifications.hide('not-implemented');
notifications.show({
title: t`Not implemented`,
message: t`This feature is not yet implemented`,
color: 'red',
id: 'not-implemented'
});
}
/**
* Show a notification that the user does not have permission to perform the action
*/
export function permissionDenied() {
notifications.show({
title: t`Permission Denied`,
message: t`You do not have permission to perform this action`,
color: 'red'
});
}
/**
* Display a notification on an invalid return code
*/
export function invalidResponse(returnCode: number) {
// TODO: Specific return code messages
notifications.show({
title: t`Invalid Return Code`,
message: t`Server returned status ${returnCode}`,
color: 'red'
});
}
/**
* Display a notification on timeout
*/
export function showTimeoutNotification() {
notifications.show({
title: t`Timeout`,
message: t`The request timed out`,
color: 'red'
});
}
+38
View File
@@ -0,0 +1,38 @@
/**
* Reduce an input string to a given length, adding an ellipsis if necessary
* @param str - String to shorten
* @param len - Length to shorten to
*/
export function shortenString({
str,
len = 100
}: {
str: string | undefined;
len?: number;
}) {
// Ensure that the string is a string
str = str ?? '';
str = str.toString();
// If the string is already short enough, return it
if (str.length <= len) {
return str;
}
// Otherwise, shorten it
const N = Math.floor(len / 2 - 1);
return `${str.slice(0, N)} ... ${str.slice(-N)}`;
}
/**
* Generate a short hash from a long string
*/
export function hashString(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
return hash.toString(36);
}
+47
View File
@@ -0,0 +1,47 @@
import { useLocalStorage } from '@mantine/hooks';
import { useCallback, useMemo } from 'react';
import type { FilterSetState, TableFilter } from '../types/Filters';
export default function useFilterSet(
filterKey: string,
initialFilters?: TableFilter[]
): FilterSetState {
// Array of active filters (saved to local storage)
const [storedFilters, setStoredFilters] = useLocalStorage<
TableFilter[] | null
>({
key: `inventree-filterset-${filterKey}`,
defaultValue: null,
sync: false,
getInitialValueInEffect: false
});
const activeFilters: TableFilter[] = useMemo(() => {
if (storedFilters == null) {
// If there are no stored filters, set initial values
const filters = initialFilters || [];
setStoredFilters(filters);
return filters;
}
return storedFilters || [];
}, [storedFilters]);
// Callback to clear all active filters from the table
const clearActiveFilters = useCallback(() => {
setStoredFilters([]);
}, []);
const setActiveFilters = useCallback(
(filters: TableFilter[]) => {
setStoredFilters(filters);
},
[setStoredFilters]
);
return {
filterKey,
activeFilters,
setActiveFilters,
clearActiveFilters
};
}
+149
View File
@@ -0,0 +1,149 @@
import { randomId } from '@mantine/hooks';
import { useCallback, useMemo, useState } from 'react';
import type { FilterSetState, TableFilter } from '../types/Filters';
import type { TableState } from '../types/Tables';
import useFilterSet from './UseFilterSet';
export type TableStateExtraProps = {
idAccessor?: string;
initialFilters?: TableFilter[];
};
/**
* A custom hook for managing the state of an <InvenTreeTable> component.
*
* Refer to the TableState type definition for more information.
*/
export default function useTable(
tableName: string,
tableProps: TableStateExtraProps = {
idAccessor: 'pk',
initialFilters: []
}
): TableState {
// Function to generate a new ID (to refresh the table)
function generateTableName() {
return `${tableName.replaceAll('-', '')}-${randomId()}`;
}
const [tableKey, setTableKey] = useState<string>(generateTableName());
// Callback used to refresh (reload) the table
const refreshTable = useCallback(
(clearSelection?: boolean) => {
setTableKey(generateTableName());
if (clearSelection) {
clearSelectedRecords();
}
},
[generateTableName]
);
const filterSet: FilterSetState = useFilterSet(
`table-${tableName}`,
tableProps.initialFilters
);
// Array of expanded records
const [expandedRecords, setExpandedRecords] = useState<any[]>([]);
// Function to determine if a record is expanded
const isRowExpanded = useCallback(
(pk: number) => {
return expandedRecords.includes(pk);
},
[expandedRecords]
);
// Array of columns which are hidden
const [hiddenColumns, setHiddenColumns] = useState<string[]>([]);
// Array of selected records
const [selectedRecords, setSelectedRecords] = useState<any[]>([]);
// Array of selected primary key values
const selectedIds = useMemo(
() => selectedRecords.map((r) => r[tableProps.idAccessor || 'pk']),
[selectedRecords]
);
const clearSelectedRecords = useCallback(() => {
setSelectedRecords([]);
}, []);
const hasSelectedRecords = useMemo(() => {
return selectedRecords.length > 0;
}, [selectedRecords]);
// Total record count
const [recordCount, setRecordCount] = useState<number>(0);
const [page, setPage] = useState<number>(1);
// Search term
const [searchTerm, setSearchTerm] = useState<string>('');
// Table records
const [records, setRecords] = useState<any[]>([]);
// Update a single record in the table, by primary key value
const updateRecord = useCallback(
(record: any) => {
const _records = [...records];
// Find the matching record in the table
const index = _records.findIndex(
(r) => r[tableProps.idAccessor || 'pk'] === record.pk
);
if (index >= 0) {
_records[index] = {
..._records[index],
...record
};
} else {
_records.push(record);
}
setRecords(_records);
},
[records]
);
const idAccessor = useMemo(
() => tableProps.idAccessor || 'pk',
[tableProps.idAccessor]
);
const [isLoading, setIsLoading] = useState<boolean>(false);
return {
tableKey,
refreshTable,
isLoading,
setIsLoading,
filterSet,
expandedRecords,
setExpandedRecords,
isRowExpanded,
selectedRecords,
selectedIds,
setSelectedRecords,
clearSelectedRecords,
hasSelectedRecords,
searchTerm,
setSearchTerm,
recordCount,
setRecordCount,
hiddenColumns,
setHiddenColumns,
page,
setPage,
records,
setRecords,
updateRecord,
idAccessor
};
}
+49 -1
View File
@@ -15,10 +15,20 @@ export { UserRoles, UserPermissions } from './enums/Roles';
export type {
InvenTreePluginContext,
InvenTreeFormsContext,
InvenTreeTablesContext,
ImporterDrawerContext,
PluginVersion,
StockAdjustmentFormsContext
} from './types/Plugins';
export type { RowAction, RowViewProps } from './types/Tables';
export type {
RowAction,
RowViewProps,
TableColumn,
TableColumnProps,
InvenTreeTableProps,
InvenTreeTableRenderProps
} from './types/Tables';
export type {
ApiFormFieldChoice,
@@ -42,6 +52,14 @@ export {
getDetailUrl,
navigateToLink
} from './functions/Navigation';
export {
notYetImplemented,
permissionDenied,
invalidResponse,
showTimeoutNotification
} from './functions/Notification';
export {
checkPluginVersion,
initPlugin
@@ -53,16 +71,32 @@ export {
formatFileSize
} from './functions/Formatting';
export {
constructFormUrl,
mapFields,
type NestedDict
} from './functions/Forms';
export {
shortenString,
hashString
} from './functions/String';
// Common UI components
export {
ActionButton,
type ActionButtonProps
} from './components/ActionButton';
export { AddItemButton } from './components/AddItemButton';
export { Boundary, DefaultFallback } from './components/Boundary';
export { ButtonMenu } from './components/ButtonMenu';
export { CopyButton } from './components/CopyButton';
export { CopyableCell } from './components/CopyableCell';
export { ProgressBar } from './components/ProgressBar';
export { PassFailButton, YesNoButton } from './components/YesNoButton';
export { SearchInput } from './components/SearchInput';
export { TableColumnSelect } from './components/TableColumnSelect';
export { default as InvenTreeTable } from './components/InvenTreeTable';
export {
RowViewAction,
RowDuplicateAction,
@@ -77,7 +111,21 @@ export {
default as useMonitorDataOutput,
type MonitorDataOutputProps
} from './hooks/MonitorDataOutput';
export {
default as useMonitorBackgroundTask,
type MonitorBackgroundTaskProps
} from './hooks/MonitorBackgroundTask';
export { default as useFilterSet } from './hooks/UseFilterSet';
export {
default as useTable,
type TableStateExtraProps
} from './hooks/UseTable';
// State management
export {
type StoredTableStateProps,
useStoredTableState
} from './states/StoredTableState';
@@ -0,0 +1,91 @@
import type { DataTableSortStatus } from 'mantine-datatable';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const DEFAULT_PAGE_SIZE: number = 25;
/**
* Interfacing for storing persistent table state in the browser.
*
* The following properties are stored:
* - pageSize: The number of rows to display per page in the table.
* - hiddenColumns: An array of column names that are hidden in a given table.
* - columnNames: An object mapping table keys to arrays of column names.
* - sorting: An object mapping table keys to sorting configurations.
*/
export interface StoredTableStateProps {
pageSize: number;
setPageSize: (size: number) => void;
tableSorting: Record<string, any>;
getTableSorting: (tableKey: string) => DataTableSortStatus;
setTableSorting: (
tableKey: string
) => (sorting: DataTableSortStatus<any>) => void;
tableColumnNames: Record<string, Record<string, string>>;
getTableColumnNames: (tableKey: string) => Record<string, string>;
setTableColumnNames: (
tableKey: string
) => (names: Record<string, string>) => void;
clearTableColumnNames: () => void;
hiddenColumns: Record<string, string[]>;
getHiddenColumns: (tableKey: string) => string[] | null;
setHiddenColumns: (tableKey: string) => (columns: string[]) => void;
}
export const useStoredTableState = create<StoredTableStateProps>()(
persist(
(set, get) => ({
pageSize: DEFAULT_PAGE_SIZE,
setPageSize: (size: number) => {
set((state) => ({
pageSize: size
}));
},
tableSorting: {},
getTableSorting: (tableKey) => {
return get().tableSorting[tableKey] || {};
},
setTableSorting: (tableKey) => (sorting) => {
// Update the table sorting for the given table
set({
tableSorting: {
...get().tableSorting,
[tableKey]: sorting
}
});
},
tableColumnNames: {},
getTableColumnNames: (tableKey) => {
return get().tableColumnNames[tableKey] || null;
},
setTableColumnNames: (tableKey) => (names) => {
// Update the table column names for the given table
set({
tableColumnNames: {
...get().tableColumnNames,
[tableKey]: names
}
});
},
clearTableColumnNames: () => {
set({ tableColumnNames: {} });
},
hiddenColumns: {},
getHiddenColumns: (tableKey) => {
return get().hiddenColumns?.[tableKey] ?? null;
},
setHiddenColumns: (tableKey) => (columns) => {
// Update the hidden columns for the given table
set({
hiddenColumns: {
...get().hiddenColumns,
[tableKey]: columns
}
});
}
}),
{
name: 'inventree-table-state'
}
)
);
+17 -9
View File
@@ -13,6 +13,7 @@ import type {
import type { UseModalReturn } from './Modals';
import type { RenderInstanceProps } from './Rendering';
import type { SettingsStateProps } from './Settings';
import type { InvenTreeTableRenderProps } from './Tables';
import type { UserStateProps } from './User';
export interface PluginProps {
@@ -48,6 +49,10 @@ export type InvenTreeFormsContext = {
stockActions: StockAdjustmentFormsContext;
};
export type InvenTreeTablesContext<T extends Record<string, any>> = {
renderTable: (props: InvenTreeTableRenderProps<T>) => React.ReactNode;
};
export type ImporterDrawerContext = {
open: (sessionId: number, options?: { onClose?: () => void }) => void;
close: () => void;
@@ -61,20 +66,22 @@ export type ImporterDrawerContext = {
*
* @param version - The version of the running InvenTree software stack
* @param api - The Axios API instance (see ../states/ApiState.tsx)
* @param queryClient - The Tanstack QueryClient instance (see ../states/QueryState.tsx)
* @param user - The current user instance (see ../states/UserState.tsx)
* @param userSettings - The current user settings (see ../states/SettingsState.tsx)
* @param globalSettings - The global settings (see ../states/SettingsState.tsx)
* @param navigate - The navigation function (see react-router-dom)
* @param theme - The current Mantine theme
* @param forms - A set of functions for opening various API forms (see ../components/Forms.tsx)
* @param importer - A set of functions for controlling the global importer drawer (see ../components/importer/GlobalImporterDrawer.tsx)
* @param colorScheme - The current Mantine color scheme (e.g. 'light' / 'dark')
* @param modelInformation - A dictionary of available model information
* @param renderInstance - A component function for rendering a model instance
* @param host - The current host URL
* @param i18n - The i18n instance for translations (from @lingui/core)
* @param locale - The current locale string (e.g. 'en' / 'de')
* @param navigate - The navigation function (see react-router-dom)
* @param theme - The current Mantine theme
* @param colorScheme - The current Mantine color scheme (e.g. 'light' / 'dark')
* @param forms - A set of functions for opening various API forms (see ../components/Forms.tsx)
* @param tables - A set of functions for rendering API tables
* @param importer - A set of functions for controlling the global importer drawer (see ../components/importer/GlobalImporterDrawer.tsx)
* @param model - The model type associated with the rendered component (if applicable)
* @param modelInformation - A dictionary of available model information
* @param renderInstance - A component function for rendering a model instance
* @param id - The ID (primary key) of the model instance for the plugin (if applicable)
* @param instance - The model instance data (if available)
* @param reloadContent - A function which can be called to reload the plugin content
@@ -95,9 +102,10 @@ export type InvenTreePluginContext = {
locale: string;
navigate: NavigateFunction;
theme: MantineTheme;
forms: InvenTreeFormsContext;
importer: ImporterDrawerContext;
colorScheme: MantineColorScheme;
forms: InvenTreeFormsContext;
tables: InvenTreeTablesContext<any>;
importer: ImporterDrawerContext;
model?: ModelType | string;
id?: string | number | null;
instance?: any;
+18 -7
View File
@@ -1,10 +1,12 @@
import type { MantineStyleProp } from '@mantine/core';
import type { AxiosInstance } from 'axios';
import type { ShowContextMenuFunction } from 'mantine-contextmenu';
import type {
DataTableCellClickHandler,
DataTableRowExpansionProps
} from 'mantine-datatable';
import type { ReactNode } from 'react';
import type { NavigateFunction, SetURLSearchParams } from 'react-router-dom';
import type { NavigateFunction } from 'react-router-dom';
import type { ModelType } from '../enums/ModelType';
import type { FilterSetState, TableFilter } from './Filters';
import type { ApiFormFieldType } from './Forms';
@@ -17,9 +19,6 @@ import type { ApiFormFieldType } from './Forms';
* isLoading: A boolean flag to indicate if the table is currently loading data
* setIsLoading: A function to set the isLoading flag
* filterSet: A group of active filters
* queryFilters: A map of query filters (e.g. ?active=true&overdue=false) passed in the URL
* setQueryFilters: A function to set the query filters
* clearQueryFilters: A function to clear all query filters
* expandedRecords: An array of expanded records (rows) in the table
* setExpandedRecords: A function to set the expanded records
* isRowExpanded: A function to determine if a record is expanded
@@ -49,9 +48,6 @@ export type TableState = {
isLoading: boolean;
setIsLoading: (value: boolean) => void;
filterSet: FilterSetState;
queryFilters: URLSearchParams;
setQueryFilters: SetURLSearchParams;
clearQueryFilters: () => void;
expandedRecords: any[];
setExpandedRecords: (records: any[]) => void;
isRowExpanded: (pk: number) => boolean;
@@ -219,3 +215,18 @@ export type InvenTreeTableProps<T = any> = {
minHeight?: number;
noHeader?: boolean;
};
export type InvenTreeTableRenderProps<T extends Record<string, any>> = {
url?: string;
tableState: TableState;
tableData?: T[];
columns: TableColumn<T>[];
props: InvenTreeTableProps<T>;
api: AxiosInstance;
navigate: NavigateFunction;
// The following attributes are for internal use only (plugins should not use these directly)
showContextMenu?: ShowContextMenuFunction;
searchParams?: URLSearchParams;
setSearchParams?: (params: URLSearchParams) => void;
};