248 lines
7.4 KiB
TypeScript
248 lines
7.4 KiB
TypeScript
import {
|
|
ActionIcon,
|
|
Divider,
|
|
Paper,
|
|
Stack,
|
|
Tabs,
|
|
Tooltip
|
|
} from '@mantine/core';
|
|
import {
|
|
IconLayoutSidebarLeftCollapse,
|
|
IconLayoutSidebarRightCollapse
|
|
} from '@tabler/icons-react';
|
|
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
|
import {
|
|
Navigate,
|
|
Route,
|
|
Routes,
|
|
useLocation,
|
|
useNavigate,
|
|
useParams
|
|
} from 'react-router-dom';
|
|
|
|
import { ModelType } from '../../enums/ModelType';
|
|
import { identifierString } from '../../functions/conversion';
|
|
import { cancelEvent } from '../../functions/events';
|
|
import { navigateToLink } from '../../functions/navigation';
|
|
import { usePluginPanels } from '../../hooks/UsePluginPanels';
|
|
import { useLocalState } from '../../states/LocalState';
|
|
import { Boundary } from '../Boundary';
|
|
import { StylishText } from '../items/StylishText';
|
|
import { PanelType } from './Panel';
|
|
|
|
/**
|
|
* Set of properties which define a panel group:
|
|
*
|
|
* @param pageKey - Unique key for this panel group
|
|
* @param panels - List of panels to display
|
|
* @param model - The target model for this panel group (e.g. 'part' / 'salesorder')
|
|
* @param id - The target ID for this panel group (set to *null* for groups which do not target a specific model instance)
|
|
* @param instance - The target model instance for this panel group
|
|
* @param selectedPanel - The currently selected panel
|
|
* @param onPanelChange - Callback when the active panel changes
|
|
* @param collapsible - If true, the panel group can be collapsed (defaults to true)
|
|
*/
|
|
export type PanelProps = {
|
|
pageKey: string;
|
|
panels: PanelType[];
|
|
instance?: any;
|
|
model?: ModelType | string;
|
|
id?: number | null;
|
|
selectedPanel?: string;
|
|
onPanelChange?: (panel: string) => void;
|
|
collapsible?: boolean;
|
|
};
|
|
|
|
function BasePanelGroup({
|
|
pageKey,
|
|
panels,
|
|
onPanelChange,
|
|
selectedPanel,
|
|
instance,
|
|
model,
|
|
id,
|
|
collapsible = true
|
|
}: Readonly<PanelProps>): ReactNode {
|
|
const localState = useLocalState();
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
|
|
const { panel } = useParams();
|
|
|
|
const [expanded, setExpanded] = useState<boolean>(true);
|
|
|
|
// Hook to load plugins for this panel
|
|
const pluginPanels = usePluginPanels({
|
|
model: model,
|
|
instance: instance,
|
|
id: id
|
|
});
|
|
|
|
const allPanels = useMemo(
|
|
() => [...panels, ...pluginPanels],
|
|
[panels, pluginPanels]
|
|
);
|
|
|
|
const activePanels = useMemo(
|
|
() => allPanels.filter((panel) => !panel.hidden && !panel.disabled),
|
|
[allPanels]
|
|
);
|
|
|
|
// Callback when the active panel changes
|
|
const handlePanelChange = useCallback(
|
|
(panel: string, event?: any) => {
|
|
if (event && (event?.ctrlKey || event?.shiftKey)) {
|
|
const url = `${location.pathname}/../${panel}`;
|
|
cancelEvent(event);
|
|
navigateToLink(url, navigate, event);
|
|
} else {
|
|
navigate(`../${panel}`);
|
|
}
|
|
|
|
localState.setLastUsedPanel(pageKey)(panel);
|
|
|
|
// Optionally call external callback hook
|
|
if (panel && onPanelChange) {
|
|
onPanelChange(panel);
|
|
}
|
|
},
|
|
[activePanels, navigate, location, onPanelChange]
|
|
);
|
|
|
|
// if the selected panel state changes update the current panel
|
|
useEffect(() => {
|
|
if (selectedPanel && selectedPanel !== panel) {
|
|
handlePanelChange(selectedPanel);
|
|
}
|
|
}, [selectedPanel, panel]);
|
|
|
|
// Determine the current panels selection (must be a valid panel)
|
|
const currentPanel: string = useMemo(() => {
|
|
if (activePanels.findIndex((p) => p.name === panel) === -1) {
|
|
return activePanels[0]?.name ?? '';
|
|
} else {
|
|
return panel ?? '';
|
|
}
|
|
}, [activePanels, panel]);
|
|
|
|
return (
|
|
<Boundary label={`PanelGroup-${pageKey}`}>
|
|
<Paper p="sm" radius="xs" shadow="xs">
|
|
<Tabs value={currentPanel} orientation="vertical" keepMounted={false}>
|
|
<Tabs.List justify="left">
|
|
{allPanels.map(
|
|
(panel) =>
|
|
!panel.hidden && (
|
|
<Tooltip
|
|
label={`tooltip-${panel.name}`}
|
|
key={panel.name}
|
|
disabled={expanded}
|
|
position="right"
|
|
>
|
|
<Tabs.Tab
|
|
p="xs"
|
|
key={`panel-label-${panel.name}`}
|
|
value={panel.name}
|
|
leftSection={panel.icon}
|
|
hidden={panel.hidden}
|
|
disabled={panel.disabled}
|
|
style={{ cursor: panel.disabled ? 'unset' : 'pointer' }}
|
|
onClick={(event: any) =>
|
|
handlePanelChange(panel.name, event)
|
|
}
|
|
>
|
|
{expanded && panel.label}
|
|
</Tabs.Tab>
|
|
</Tooltip>
|
|
)
|
|
)}
|
|
{collapsible && (
|
|
<ActionIcon
|
|
style={{
|
|
paddingLeft: '10px'
|
|
}}
|
|
onClick={() => setExpanded(!expanded)}
|
|
variant="transparent"
|
|
size="md"
|
|
>
|
|
{expanded ? (
|
|
<IconLayoutSidebarLeftCollapse opacity={0.5} />
|
|
) : (
|
|
<IconLayoutSidebarRightCollapse opacity={0.5} />
|
|
)}
|
|
</ActionIcon>
|
|
)}
|
|
</Tabs.List>
|
|
{allPanels.map(
|
|
(panel) =>
|
|
!panel.hidden && (
|
|
<Tabs.Panel
|
|
key={`panel-${panel.name}`}
|
|
value={panel.name}
|
|
aria-label={`nav-panel-${identifierString(
|
|
`${pageKey}-${panel.name}`
|
|
)}`}
|
|
p="sm"
|
|
style={{
|
|
overflowX: 'scroll',
|
|
width: '100%'
|
|
}}
|
|
>
|
|
<Stack gap="md">
|
|
{panel.showHeadline !== false && (
|
|
<>
|
|
<StylishText size="xl">{panel.label}</StylishText>
|
|
<Divider />
|
|
</>
|
|
)}
|
|
<Boundary label={`PanelContent-${panel.name}`}>
|
|
{panel.content}
|
|
</Boundary>
|
|
</Stack>
|
|
</Tabs.Panel>
|
|
)
|
|
)}
|
|
</Tabs>
|
|
</Paper>
|
|
</Boundary>
|
|
);
|
|
}
|
|
|
|
function IndexPanelComponent({
|
|
pageKey,
|
|
selectedPanel,
|
|
panels
|
|
}: Readonly<PanelProps>) {
|
|
const lastUsedPanel = useLocalState((state) => {
|
|
const panelName =
|
|
selectedPanel || state.lastUsedPanels[pageKey] || panels[0]?.name;
|
|
|
|
const panel = panels.findIndex(
|
|
(p) => p.name === panelName && !p.disabled && !p.hidden
|
|
);
|
|
if (panel === -1) {
|
|
return panels.find((p) => !p.disabled && !p.hidden)?.name || '';
|
|
}
|
|
|
|
return panelName;
|
|
});
|
|
|
|
return <Navigate to={lastUsedPanel} replace />;
|
|
}
|
|
|
|
/**
|
|
* Render a panel group. The current panel will be appended to the current url.
|
|
* The last opened panel will be stored in local storage and opened if no panel is provided via url param
|
|
* @param panels - The list of panels to display
|
|
* @param onPanelChange - Callback when the active panel changes
|
|
* @param collapsible - If true, the panel group can be collapsed (defaults to true)
|
|
*/
|
|
export function PanelGroup(props: Readonly<PanelProps>) {
|
|
return (
|
|
<Routes>
|
|
<Route index element={<IndexPanelComponent {...props} />} />
|
|
<Route path="/:panel/*" element={<BasePanelGroup {...props} />} />
|
|
</Routes>
|
|
);
|
|
}
|