mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[PUI] Error boundary (#7176)
* Create error boundary component - Ref: https://docs.sentry.io/platforms/javascript/guides/react/features/error-boundary/ - Keeps errors container to local components - Will be critical for plugin support * Add boundary to API forms
This commit is contained in:
parent
c7351c4064
commit
108bd28102
44
src/frontend/src/components/Boundary.tsx
Normal file
44
src/frontend/src/components/Boundary.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Alert } from '@mantine/core';
|
||||||
|
import { ErrorBoundary, FallbackRender } from '@sentry/react';
|
||||||
|
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||||
|
import { ReactNode, useCallback } from 'react';
|
||||||
|
|
||||||
|
function DefaultFallback({ title }: { 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
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
label: string;
|
||||||
|
fallback?: React.ReactElement | FallbackRender | undefined;
|
||||||
|
}): ReactNode {
|
||||||
|
const onError = useCallback(
|
||||||
|
(error: Error, componentStack: string, eventId: string) => {
|
||||||
|
console.error(`Error rendering component: ${label}`);
|
||||||
|
console.error(error, componentStack);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary
|
||||||
|
fallback={fallback ?? <DefaultFallback title={label} />}
|
||||||
|
onError={onError}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
@ -36,6 +36,7 @@ import {
|
|||||||
import { invalidResponse } from '../../functions/notifications';
|
import { invalidResponse } from '../../functions/notifications';
|
||||||
import { getDetailUrl } from '../../functions/urls';
|
import { getDetailUrl } from '../../functions/urls';
|
||||||
import { PathParams } from '../../states/ApiState';
|
import { PathParams } from '../../states/ApiState';
|
||||||
|
import { Boundary } from '../Boundary';
|
||||||
import {
|
import {
|
||||||
ApiFormField,
|
ApiFormField,
|
||||||
ApiFormFieldSet,
|
ApiFormFieldSet,
|
||||||
@ -472,81 +473,89 @@ export function ApiForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
{/* Show loading overlay while fetching fields */}
|
<Boundary label={`ApiForm-${id}`}>
|
||||||
{/* zIndex used to force overlay on top of modal header bar */}
|
{/* Show loading overlay while fetching fields */}
|
||||||
<LoadingOverlay visible={isLoading} zIndex={1010} />
|
{/* zIndex used to force overlay on top of modal header bar */}
|
||||||
|
<LoadingOverlay visible={isLoading} zIndex={1010} />
|
||||||
|
|
||||||
{/* Attempt at making fixed footer with scroll area */}
|
{/* Attempt at making fixed footer with scroll area */}
|
||||||
<Paper mah={'65vh'} style={{ overflowY: 'auto' }}>
|
<Paper mah={'65vh'} style={{ overflowY: 'auto' }}>
|
||||||
<div>
|
<div>
|
||||||
{/* Form Fields */}
|
{/* Form Fields */}
|
||||||
<Stack spacing="sm">
|
<Stack spacing="sm">
|
||||||
{(!isValid || nonFieldErrors.length > 0) && (
|
{(!isValid || nonFieldErrors.length > 0) && (
|
||||||
<Alert radius="sm" color="red" title={t`Form Errors Exist`}>
|
<Alert radius="sm" color="red" title={t`Form Errors Exist`}>
|
||||||
{nonFieldErrors.length > 0 && (
|
{nonFieldErrors.length > 0 && (
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
{nonFieldErrors.map((message) => (
|
{nonFieldErrors.map((message) => (
|
||||||
<Text key={message}>{message}</Text>
|
<Text key={message}>{message}</Text>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<Boundary label={`ApiForm-${id}-PreFormContent`}>
|
||||||
|
{props.preFormContent}
|
||||||
|
{props.preFormSuccess && (
|
||||||
|
<Alert color="green" radius="sm">
|
||||||
|
{props.preFormSuccess}
|
||||||
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</Alert>
|
{props.preFormWarning && (
|
||||||
)}
|
<Alert color="orange" radius="sm">
|
||||||
{props.preFormContent}
|
{props.preFormWarning}
|
||||||
{props.preFormSuccess && (
|
</Alert>
|
||||||
<Alert color="green" radius="sm">
|
)}
|
||||||
{props.preFormSuccess}
|
</Boundary>
|
||||||
</Alert>
|
<Boundary label={`ApiForm-${id}-FormContent`}>
|
||||||
)}
|
<FormProvider {...form}>
|
||||||
{props.preFormWarning && (
|
<Stack spacing="xs">
|
||||||
<Alert color="orange" radius="sm">
|
{!optionsLoading &&
|
||||||
{props.preFormWarning}
|
Object.entries(fields).map(([fieldName, field]) => (
|
||||||
</Alert>
|
<ApiFormField
|
||||||
)}
|
key={fieldName}
|
||||||
<FormProvider {...form}>
|
fieldName={fieldName}
|
||||||
<Stack spacing="xs">
|
definition={field}
|
||||||
{!optionsLoading &&
|
control={form.control}
|
||||||
Object.entries(fields).map(([fieldName, field]) => (
|
/>
|
||||||
<ApiFormField
|
))}
|
||||||
key={fieldName}
|
</Stack>
|
||||||
fieldName={fieldName}
|
</FormProvider>
|
||||||
definition={field}
|
</Boundary>
|
||||||
control={form.control}
|
<Boundary label={`ApiForm-${id}-PostFormContent`}>
|
||||||
/>
|
{props.postFormContent}
|
||||||
))}
|
</Boundary>
|
||||||
</Stack>
|
</Stack>
|
||||||
</FormProvider>
|
</div>
|
||||||
{props.postFormContent}
|
</Paper>
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{/* Footer with Action Buttons */}
|
{/* Footer with Action Buttons */}
|
||||||
<Divider />
|
<Divider />
|
||||||
<div>
|
<div>
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
{props.actions?.map((action, i) => (
|
{props.actions?.map((action, i) => (
|
||||||
|
<Button
|
||||||
|
key={i}
|
||||||
|
onClick={action.onClick}
|
||||||
|
variant={action.variant ?? 'outline'}
|
||||||
|
radius="sm"
|
||||||
|
color={action.color}
|
||||||
|
>
|
||||||
|
{action.text}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
<Button
|
<Button
|
||||||
key={i}
|
onClick={form.handleSubmit(submitForm, onFormError)}
|
||||||
onClick={action.onClick}
|
variant="filled"
|
||||||
variant={action.variant ?? 'outline'}
|
|
||||||
radius="sm"
|
radius="sm"
|
||||||
color={action.color}
|
color={props.submitColor ?? 'green'}
|
||||||
|
disabled={isLoading || (props.fetchInitialData && !isDirty)}
|
||||||
>
|
>
|
||||||
{action.text}
|
{props.submitText ?? t`Submit`}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
</Group>
|
||||||
<Button
|
</div>
|
||||||
onClick={form.handleSubmit(submitForm, onFormError)}
|
</Boundary>
|
||||||
variant="filled"
|
|
||||||
radius="sm"
|
|
||||||
color={props.submitColor ?? 'green'}
|
|
||||||
disabled={isLoading || (props.fetchInitialData && !isDirty)}
|
|
||||||
>
|
|
||||||
{props.submitText ?? t`Submit`}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|||||||
import { getActions } from '../../defaults/actions';
|
import { getActions } from '../../defaults/actions';
|
||||||
import { InvenTreeStyle } from '../../globalStyle';
|
import { InvenTreeStyle } from '../../globalStyle';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
|
import { Boundary } from '../Boundary';
|
||||||
import { Footer } from './Footer';
|
import { Footer } from './Footer';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
|
|
||||||
@ -29,17 +30,17 @@ export default function LayoutComponent() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const defaultactions = getActions(navigate);
|
const defaultActions = getActions(navigate);
|
||||||
const [actions, setActions] = useState(defaultactions);
|
const [actions, setActions] = useState(defaultActions);
|
||||||
const [customActions, setCustomActions] = useState<boolean>(false);
|
const [customActions, setCustomActions] = useState<boolean>(false);
|
||||||
|
|
||||||
function actionsAreChanging(change: []) {
|
function actionsAreChanging(change: []) {
|
||||||
if (change.length > defaultactions.length) setCustomActions(true);
|
if (change.length > defaultActions.length) setCustomActions(true);
|
||||||
setActions(change);
|
setActions(change);
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (customActions) {
|
if (customActions) {
|
||||||
setActions(defaultactions);
|
setActions(defaultActions);
|
||||||
setCustomActions(false);
|
setCustomActions(false);
|
||||||
}
|
}
|
||||||
}, [location]);
|
}, [location]);
|
||||||
@ -57,7 +58,10 @@ export default function LayoutComponent() {
|
|||||||
<Flex direction="column" mih="100vh">
|
<Flex direction="column" mih="100vh">
|
||||||
<Header />
|
<Header />
|
||||||
<Container className={classes.layoutContent} size="100%">
|
<Container className={classes.layoutContent} size="100%">
|
||||||
<Outlet />
|
<Boundary label={'layout'}>
|
||||||
|
<Outlet />
|
||||||
|
</Boundary>
|
||||||
|
{/* </ErrorBoundary> */}
|
||||||
</Container>
|
</Container>
|
||||||
<Space h="xl" />
|
<Space h="xl" />
|
||||||
<Footer />
|
<Footer />
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import { useLocalState } from '../../states/LocalState';
|
import { useLocalState } from '../../states/LocalState';
|
||||||
|
import { Boundary } from '../Boundary';
|
||||||
import { PlaceholderPanel } from '../items/Placeholder';
|
import { PlaceholderPanel } from '../items/Placeholder';
|
||||||
import { StylishText } from '../items/StylishText';
|
import { StylishText } from '../items/StylishText';
|
||||||
|
|
||||||
@ -103,77 +104,81 @@ function BasePanelGroup({
|
|||||||
const [expanded, setExpanded] = useState<boolean>(true);
|
const [expanded, setExpanded] = useState<boolean>(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper p="sm" radius="xs" shadow="xs">
|
<Boundary label={`PanelGroup-${pageKey}`}>
|
||||||
<Tabs
|
<Paper p="sm" radius="xs" shadow="xs">
|
||||||
value={panel}
|
<Tabs
|
||||||
orientation="vertical"
|
value={panel}
|
||||||
onTabChange={handlePanelChange}
|
orientation="vertical"
|
||||||
keepMounted={false}
|
onTabChange={handlePanelChange}
|
||||||
>
|
keepMounted={false}
|
||||||
<Tabs.List position="left">
|
>
|
||||||
|
<Tabs.List position="left">
|
||||||
|
{panels.map(
|
||||||
|
(panel) =>
|
||||||
|
!panel.hidden && (
|
||||||
|
<Tooltip
|
||||||
|
label={panel.label}
|
||||||
|
key={panel.name}
|
||||||
|
disabled={expanded}
|
||||||
|
>
|
||||||
|
<Tabs.Tab
|
||||||
|
p="xs"
|
||||||
|
value={panel.name}
|
||||||
|
// icon={(<InvenTreeIcon icon={panel.name}/>)} // Enable when implementing Icon manager everywhere
|
||||||
|
icon={panel.icon}
|
||||||
|
hidden={panel.hidden}
|
||||||
|
disabled={panel.disabled}
|
||||||
|
style={{ cursor: panel.disabled ? 'unset' : 'pointer' }}
|
||||||
|
>
|
||||||
|
{expanded && panel.label}
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{collapsible && (
|
||||||
|
<ActionIcon
|
||||||
|
style={{
|
||||||
|
paddingLeft: '10px'
|
||||||
|
}}
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
>
|
||||||
|
{expanded ? (
|
||||||
|
<IconLayoutSidebarLeftCollapse opacity={0.5} />
|
||||||
|
) : (
|
||||||
|
<IconLayoutSidebarRightCollapse opacity={0.5} />
|
||||||
|
)}
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
</Tabs.List>
|
||||||
{panels.map(
|
{panels.map(
|
||||||
(panel) =>
|
(panel) =>
|
||||||
!panel.hidden && (
|
!panel.hidden && (
|
||||||
<Tooltip
|
<Tabs.Panel
|
||||||
label={panel.label}
|
|
||||||
key={panel.name}
|
key={panel.name}
|
||||||
disabled={expanded}
|
value={panel.name}
|
||||||
|
p="sm"
|
||||||
|
style={{
|
||||||
|
overflowX: 'scroll',
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tabs.Tab
|
<Stack spacing="md">
|
||||||
p="xs"
|
{panel.showHeadline !== false && (
|
||||||
value={panel.name}
|
<>
|
||||||
// icon={(<InvenTreeIcon icon={panel.name}/>)} // Enable when implementing Icon manager everywhere
|
<StylishText size="xl">{panel.label}</StylishText>
|
||||||
icon={panel.icon}
|
<Divider />
|
||||||
hidden={panel.hidden}
|
</>
|
||||||
disabled={panel.disabled}
|
)}
|
||||||
style={{ cursor: panel.disabled ? 'unset' : 'pointer' }}
|
<Boundary label={`PanelContent-${panel.name}`}>
|
||||||
>
|
{panel.content ?? <PlaceholderPanel />}
|
||||||
{expanded && panel.label}
|
</Boundary>
|
||||||
</Tabs.Tab>
|
</Stack>
|
||||||
</Tooltip>
|
</Tabs.Panel>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{collapsible && (
|
</Tabs>
|
||||||
<ActionIcon
|
</Paper>
|
||||||
style={{
|
</Boundary>
|
||||||
paddingLeft: '10px'
|
|
||||||
}}
|
|
||||||
onClick={() => setExpanded(!expanded)}
|
|
||||||
>
|
|
||||||
{expanded ? (
|
|
||||||
<IconLayoutSidebarLeftCollapse opacity={0.5} />
|
|
||||||
) : (
|
|
||||||
<IconLayoutSidebarRightCollapse opacity={0.5} />
|
|
||||||
)}
|
|
||||||
</ActionIcon>
|
|
||||||
)}
|
|
||||||
</Tabs.List>
|
|
||||||
{panels.map(
|
|
||||||
(panel) =>
|
|
||||||
!panel.hidden && (
|
|
||||||
<Tabs.Panel
|
|
||||||
key={panel.name}
|
|
||||||
value={panel.name}
|
|
||||||
p="sm"
|
|
||||||
style={{
|
|
||||||
overflowX: 'scroll',
|
|
||||||
width: '100%'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack spacing="md">
|
|
||||||
{panel.showHeadline !== false && (
|
|
||||||
<>
|
|
||||||
<StylishText size="xl">{panel.label}</StylishText>
|
|
||||||
<Divider />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{panel.content ?? <PlaceholderPanel />}
|
|
||||||
</Stack>
|
|
||||||
</Tabs.Panel>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Tabs>
|
|
||||||
</Paper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ import { UserRoles } from '../../enums/Roles';
|
|||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserSettingsState } from '../../states/SettingsState';
|
import { useUserSettingsState } from '../../states/SettingsState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
|
import { Boundary } from '../Boundary';
|
||||||
import { RenderInstance } from '../render/Instance';
|
import { RenderInstance } from '../render/Instance';
|
||||||
import { ModelInformationDict } from '../render/ModelType';
|
import { ModelInformationDict } from '../render/ModelType';
|
||||||
|
|
||||||
@ -386,48 +387,50 @@ export function SearchDrawer({
|
|||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{searchQuery.isFetching && (
|
<Boundary label="SearchDrawer">
|
||||||
<Center>
|
{searchQuery.isFetching && (
|
||||||
<Loader />
|
<Center>
|
||||||
</Center>
|
<Loader />
|
||||||
)}
|
</Center>
|
||||||
{!searchQuery.isFetching && !searchQuery.isError && (
|
)}
|
||||||
<Stack spacing="md">
|
{!searchQuery.isFetching && !searchQuery.isError && (
|
||||||
{queryResults.map((query, idx) => (
|
<Stack spacing="md">
|
||||||
<QueryResultGroup
|
{queryResults.map((query, idx) => (
|
||||||
key={idx}
|
<QueryResultGroup
|
||||||
query={query}
|
key={idx}
|
||||||
onRemove={(query) => removeResults(query)}
|
query={query}
|
||||||
onResultClick={(query, pk) => onResultClick(query, pk)}
|
onRemove={(query) => removeResults(query)}
|
||||||
/>
|
onResultClick={(query, pk) => onResultClick(query, pk)}
|
||||||
))}
|
/>
|
||||||
</Stack>
|
))}
|
||||||
)}
|
</Stack>
|
||||||
{searchQuery.isError && (
|
)}
|
||||||
<Alert
|
{searchQuery.isError && (
|
||||||
color="red"
|
|
||||||
radius="sm"
|
|
||||||
variant="light"
|
|
||||||
title={t`Error`}
|
|
||||||
icon={<IconAlertCircle size="1rem" />}
|
|
||||||
>
|
|
||||||
<Trans>An error occurred during search query</Trans>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{searchText &&
|
|
||||||
!searchQuery.isFetching &&
|
|
||||||
!searchQuery.isError &&
|
|
||||||
queryResults.length == 0 && (
|
|
||||||
<Alert
|
<Alert
|
||||||
color="blue"
|
color="red"
|
||||||
radius="sm"
|
radius="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
title={t`No results`}
|
title={t`Error`}
|
||||||
icon={<IconSearch size="1rem" />}
|
icon={<IconAlertCircle size="1rem" />}
|
||||||
>
|
>
|
||||||
<Trans>No results available for search query</Trans>
|
<Trans>An error occurred during search query</Trans>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
{searchText &&
|
||||||
|
!searchQuery.isFetching &&
|
||||||
|
!searchQuery.isError &&
|
||||||
|
queryResults.length == 0 && (
|
||||||
|
<Alert
|
||||||
|
color="blue"
|
||||||
|
radius="sm"
|
||||||
|
variant="light"
|
||||||
|
title={t`No results`}
|
||||||
|
icon={<IconSearch size="1rem" />}
|
||||||
|
>
|
||||||
|
<Trans>No results available for search query</Trans>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Boundary>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { api } from '../App';
|
import { api } from '../App';
|
||||||
|
import { Boundary } from '../components/Boundary';
|
||||||
import { ActionButton } from '../components/buttons/ActionButton';
|
import { ActionButton } from '../components/buttons/ActionButton';
|
||||||
import { ButtonMenu } from '../components/buttons/ButtonMenu';
|
import { ButtonMenu } from '../components/buttons/ButtonMenu';
|
||||||
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
|
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
|
||||||
@ -557,131 +558,135 @@ export function InvenTreeTable<T = any>({
|
|||||||
onClose={() => setFiltersVisible(false)}
|
onClose={() => setFiltersVisible(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Stack spacing="sm">
|
<Boundary label="inventreetable">
|
||||||
<Group position="apart">
|
<Stack spacing="sm">
|
||||||
<Group position="left" key="custom-actions" spacing={5}>
|
<Group position="apart">
|
||||||
{tableProps.tableActions?.map((group, idx) => (
|
<Group position="left" key="custom-actions" spacing={5}>
|
||||||
<Fragment key={idx}>{group}</Fragment>
|
{tableProps.tableActions?.map((group, idx) => (
|
||||||
))}
|
<Fragment key={idx}>{group}</Fragment>
|
||||||
{(tableProps.barcodeActions?.length ?? 0 > 0) && (
|
))}
|
||||||
<ButtonMenu
|
{(tableProps.barcodeActions?.length ?? 0 > 0) && (
|
||||||
key="barcode-actions"
|
<ButtonMenu
|
||||||
icon={<IconBarcode />}
|
key="barcode-actions"
|
||||||
label={t`Barcode actions`}
|
icon={<IconBarcode />}
|
||||||
tooltip={t`Barcode actions`}
|
label={t`Barcode actions`}
|
||||||
actions={tableProps.barcodeActions ?? []}
|
tooltip={t`Barcode actions`}
|
||||||
/>
|
actions={tableProps.barcodeActions ?? []}
|
||||||
)}
|
/>
|
||||||
{(tableProps.printingActions?.length ?? 0 > 0) && (
|
)}
|
||||||
<ButtonMenu
|
{(tableProps.printingActions?.length ?? 0 > 0) && (
|
||||||
key="printing-actions"
|
<ButtonMenu
|
||||||
icon={<IconPrinter />}
|
key="printing-actions"
|
||||||
label={t`Print actions`}
|
icon={<IconPrinter />}
|
||||||
tooltip={t`Print actions`}
|
label={t`Print actions`}
|
||||||
actions={tableProps.printingActions ?? []}
|
tooltip={t`Print actions`}
|
||||||
/>
|
actions={tableProps.printingActions ?? []}
|
||||||
)}
|
/>
|
||||||
{(tableProps.enableBulkDelete ?? false) && (
|
)}
|
||||||
<ActionButton
|
{(tableProps.enableBulkDelete ?? false) && (
|
||||||
disabled={tableState.selectedRecords.length == 0}
|
<ActionButton
|
||||||
icon={<IconTrash />}
|
disabled={tableState.selectedRecords.length == 0}
|
||||||
color="red"
|
icon={<IconTrash />}
|
||||||
tooltip={t`Delete selected records`}
|
color="red"
|
||||||
onClick={deleteSelectedRecords}
|
tooltip={t`Delete selected records`}
|
||||||
/>
|
onClick={deleteSelectedRecords}
|
||||||
)}
|
/>
|
||||||
</Group>
|
)}
|
||||||
<Space />
|
</Group>
|
||||||
<Group position="right" spacing={5}>
|
<Space />
|
||||||
{tableProps.enableSearch && (
|
<Group position="right" spacing={5}>
|
||||||
<TableSearchInput
|
{tableProps.enableSearch && (
|
||||||
searchCallback={(term: string) =>
|
<TableSearchInput
|
||||||
tableState.setSearchTerm(term)
|
searchCallback={(term: string) =>
|
||||||
}
|
tableState.setSearchTerm(term)
|
||||||
/>
|
}
|
||||||
)}
|
/>
|
||||||
{tableProps.enableRefresh && (
|
)}
|
||||||
<ActionIcon>
|
{tableProps.enableRefresh && (
|
||||||
<Tooltip label={t`Refresh data`}>
|
|
||||||
<IconRefresh onClick={() => refetch()} />
|
|
||||||
</Tooltip>
|
|
||||||
</ActionIcon>
|
|
||||||
)}
|
|
||||||
{hasSwitchableColumns && (
|
|
||||||
<TableColumnSelect
|
|
||||||
columns={dataColumns}
|
|
||||||
onToggleColumn={toggleColumn}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{tableProps.enableFilters && filters.length > 0 && (
|
|
||||||
<Indicator
|
|
||||||
size="xs"
|
|
||||||
label={tableState.activeFilters?.length ?? 0}
|
|
||||||
disabled={tableState.activeFilters?.length == 0}
|
|
||||||
>
|
|
||||||
<ActionIcon>
|
<ActionIcon>
|
||||||
<Tooltip label={t`Table filters`}>
|
<Tooltip label={t`Refresh data`}>
|
||||||
<IconFilter
|
<IconRefresh onClick={() => refetch()} />
|
||||||
onClick={() => setFiltersVisible(!filtersVisible)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Indicator>
|
)}
|
||||||
)}
|
{hasSwitchableColumns && (
|
||||||
{tableProps.enableDownload && (
|
<TableColumnSelect
|
||||||
<DownloadAction
|
columns={dataColumns}
|
||||||
key="download-action"
|
onToggleColumn={toggleColumn}
|
||||||
downloadCallback={downloadData}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
{tableProps.enableFilters && filters.length > 0 && (
|
||||||
|
<Indicator
|
||||||
|
size="xs"
|
||||||
|
label={tableState.activeFilters?.length ?? 0}
|
||||||
|
disabled={tableState.activeFilters?.length == 0}
|
||||||
|
>
|
||||||
|
<ActionIcon>
|
||||||
|
<Tooltip label={t`Table filters`}>
|
||||||
|
<IconFilter
|
||||||
|
onClick={() => setFiltersVisible(!filtersVisible)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</ActionIcon>
|
||||||
|
</Indicator>
|
||||||
|
)}
|
||||||
|
{tableProps.enableDownload && (
|
||||||
|
<DownloadAction
|
||||||
|
key="download-action"
|
||||||
|
downloadCallback={downloadData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
<Box pos="relative">
|
||||||
<Box pos="relative">
|
<LoadingOverlay
|
||||||
<LoadingOverlay
|
visible={
|
||||||
visible={tableOptionQuery.isLoading || tableOptionQuery.isFetching}
|
tableOptionQuery.isLoading || tableOptionQuery.isFetching
|
||||||
/>
|
|
||||||
|
|
||||||
<DataTable
|
|
||||||
withBorder
|
|
||||||
striped
|
|
||||||
highlightOnHover
|
|
||||||
loaderVariant="dots"
|
|
||||||
pinLastColumn={tableProps.rowActions != undefined}
|
|
||||||
idAccessor={tableProps.idAccessor}
|
|
||||||
minHeight={300}
|
|
||||||
totalRecords={tableState.recordCount}
|
|
||||||
recordsPerPage={tableProps.pageSize ?? defaultPageSize}
|
|
||||||
page={tableState.page}
|
|
||||||
onPageChange={tableState.setPage}
|
|
||||||
sortStatus={sortStatus}
|
|
||||||
onSortStatusChange={handleSortStatusChange}
|
|
||||||
selectedRecords={
|
|
||||||
tableProps.enableSelection
|
|
||||||
? tableState.selectedRecords
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onSelectedRecordsChange={
|
|
||||||
tableProps.enableSelection ? onSelectedRecordsChange : undefined
|
|
||||||
}
|
|
||||||
rowExpansion={tableProps.rowExpansion}
|
|
||||||
rowStyle={tableProps.rowStyle}
|
|
||||||
fetching={isFetching}
|
|
||||||
noRecordsText={missingRecordsText}
|
|
||||||
records={tableState.records}
|
|
||||||
columns={dataColumns}
|
|
||||||
onRowClick={handleRowClick}
|
|
||||||
onCellClick={tableProps.onCellClick}
|
|
||||||
defaultColumnProps={{
|
|
||||||
noWrap: true,
|
|
||||||
textAlignment: 'left',
|
|
||||||
cellsStyle: {
|
|
||||||
// TODO @SchrodingersGat : Need a better way of handling "wide" cells,
|
|
||||||
overflow: 'hidden'
|
|
||||||
}
|
}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</Box>
|
<DataTable
|
||||||
</Stack>
|
withBorder
|
||||||
|
striped
|
||||||
|
highlightOnHover
|
||||||
|
loaderVariant="dots"
|
||||||
|
pinLastColumn={tableProps.rowActions != undefined}
|
||||||
|
idAccessor={tableProps.idAccessor}
|
||||||
|
minHeight={300}
|
||||||
|
totalRecords={tableState.recordCount}
|
||||||
|
recordsPerPage={tableProps.pageSize ?? defaultPageSize}
|
||||||
|
page={tableState.page}
|
||||||
|
onPageChange={tableState.setPage}
|
||||||
|
sortStatus={sortStatus}
|
||||||
|
onSortStatusChange={handleSortStatusChange}
|
||||||
|
selectedRecords={
|
||||||
|
tableProps.enableSelection
|
||||||
|
? tableState.selectedRecords
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onSelectedRecordsChange={
|
||||||
|
tableProps.enableSelection ? onSelectedRecordsChange : undefined
|
||||||
|
}
|
||||||
|
rowExpansion={tableProps.rowExpansion}
|
||||||
|
rowStyle={tableProps.rowStyle}
|
||||||
|
fetching={isFetching}
|
||||||
|
noRecordsText={missingRecordsText}
|
||||||
|
records={tableState.records}
|
||||||
|
columns={dataColumns}
|
||||||
|
onRowClick={handleRowClick}
|
||||||
|
onCellClick={tableProps.onCellClick}
|
||||||
|
defaultColumnProps={{
|
||||||
|
noWrap: true,
|
||||||
|
textAlignment: 'left',
|
||||||
|
cellsStyle: {
|
||||||
|
// TODO @SchrodingersGat : Need a better way of handling "wide" cells,
|
||||||
|
overflow: 'hidden'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Boundary>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user