mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-02 10:18:42 +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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user