mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-15 03:25:42 +00:00
feat(frontend): add grouping in the admin center (#9596)
* feat(frontend): Add grouping in the admin center * fix name * make sections more distinct * re-arrange sections * ensure plugin panels work * fix missing key issue * fix test name
This commit is contained in:
@ -13,3 +13,10 @@ export type PanelType = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
showHeadline?: boolean;
|
showHeadline?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PanelGroupType = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
panelIDs?: string[];
|
||||||
|
panels?: PanelType[];
|
||||||
|
};
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
|
Box,
|
||||||
Divider,
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Tabs,
|
Tabs,
|
||||||
|
Text,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
@ -31,13 +33,15 @@ import {
|
|||||||
import type { ModelType } from '@lib/enums/ModelType';
|
import type { ModelType } from '@lib/enums/ModelType';
|
||||||
import { cancelEvent } from '@lib/functions/Events';
|
import { cancelEvent } from '@lib/functions/Events';
|
||||||
import { navigateToLink } from '@lib/functions/Navigation';
|
import { navigateToLink } from '@lib/functions/Navigation';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { identifierString } from '../../functions/conversion';
|
import { identifierString } from '../../functions/conversion';
|
||||||
import { usePluginPanels } from '../../hooks/UsePluginPanels';
|
import { usePluginPanels } from '../../hooks/UsePluginPanels';
|
||||||
import { useLocalState } from '../../states/LocalState';
|
import { useLocalState } from '../../states/LocalState';
|
||||||
|
import { vars } from '../../theme';
|
||||||
import { Boundary } from '../Boundary';
|
import { Boundary } from '../Boundary';
|
||||||
import { StylishText } from '../items/StylishText';
|
import { StylishText } from '../items/StylishText';
|
||||||
import type { PanelType } from '../panels/Panel';
|
import type { PanelGroupType, PanelType } from '../panels/Panel';
|
||||||
import * as classes from './PanelGroup.css';
|
import * as classes from './PanelGroup.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,6 +60,7 @@ import * as classes from './PanelGroup.css';
|
|||||||
export type PanelProps = {
|
export type PanelProps = {
|
||||||
pageKey: string;
|
pageKey: string;
|
||||||
panels: PanelType[];
|
panels: PanelType[];
|
||||||
|
groups?: PanelGroupType[];
|
||||||
instance?: any;
|
instance?: any;
|
||||||
reloadInstance?: () => void;
|
reloadInstance?: () => void;
|
||||||
model?: ModelType | string;
|
model?: ModelType | string;
|
||||||
@ -63,18 +68,21 @@ export type PanelProps = {
|
|||||||
selectedPanel?: string;
|
selectedPanel?: string;
|
||||||
onPanelChange?: (panel: string) => void;
|
onPanelChange?: (panel: string) => void;
|
||||||
collapsible?: boolean;
|
collapsible?: boolean;
|
||||||
|
markCustomPanels?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function BasePanelGroup({
|
function BasePanelGroup({
|
||||||
pageKey,
|
pageKey,
|
||||||
panels,
|
panels,
|
||||||
|
groups,
|
||||||
onPanelChange,
|
onPanelChange,
|
||||||
selectedPanel,
|
selectedPanel,
|
||||||
reloadInstance,
|
reloadInstance,
|
||||||
instance,
|
instance,
|
||||||
model,
|
model,
|
||||||
id,
|
id,
|
||||||
collapsible = true
|
collapsible = true,
|
||||||
|
markCustomPanels = false
|
||||||
}: Readonly<PanelProps>): ReactNode {
|
}: Readonly<PanelProps>): ReactNode {
|
||||||
const localState = useLocalState();
|
const localState = useLocalState();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -93,29 +101,66 @@ function BasePanelGroup({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Rebuild the list of panels
|
// Rebuild the list of panels
|
||||||
const allPanels = useMemo(() => {
|
const [allPanels, groupedPanels] = useMemo(() => {
|
||||||
|
const _grouped_panels: PanelGroupType[] = [];
|
||||||
const _panels = [...panels];
|
const _panels = [...panels];
|
||||||
|
const _allpanels: PanelType[] = [...panels];
|
||||||
|
|
||||||
|
groups?.forEach((group) => {
|
||||||
|
const newVal: any = { ...group, panels: [] };
|
||||||
|
// Add panel to group and remove from main list
|
||||||
|
group.panelIDs?.forEach((panelID) => {
|
||||||
|
const index = _panels.findIndex((p) => p.name === panelID);
|
||||||
|
if (index !== -1) {
|
||||||
|
newVal.panels.push(_panels[index]);
|
||||||
|
_panels.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_grouped_panels.push(newVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add remaining panels to group
|
||||||
|
if (_panels.length > 0) {
|
||||||
|
_grouped_panels.push({
|
||||||
|
id: 'ungrouped',
|
||||||
|
label: '',
|
||||||
|
panels: _panels
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add plugin panels
|
// Add plugin panels
|
||||||
|
const pluginPanels: any = [];
|
||||||
pluginPanelSet.panels?.forEach((panel) => {
|
pluginPanelSet.panels?.forEach((panel) => {
|
||||||
let panelKey = panel.name;
|
let panelKey = panel.name;
|
||||||
|
|
||||||
// Check if panel with this name already exists
|
// Check if panel with this name already exists
|
||||||
const existingPanel = _panels.find((p) => p.name === panelKey);
|
const existingPanel = panels.find((p) => p.name === panelKey);
|
||||||
|
|
||||||
if (existingPanel) {
|
if (existingPanel) {
|
||||||
// Create a unique key for the panel which includes the plugin slug
|
// Create a unique key for the panel which includes the plugin slug
|
||||||
panelKey = identifierString(`${panel.pluginName}-${panel.name}`);
|
panelKey = identifierString(`${panel.pluginName}-${panel.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
_panels.push({
|
pluginPanels.push({
|
||||||
|
...panel,
|
||||||
|
name: panelKey
|
||||||
|
});
|
||||||
|
_allpanels.push({
|
||||||
...panel,
|
...panel,
|
||||||
name: panelKey
|
name: panelKey
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return _panels;
|
if (pluginPanels.length > 0) {
|
||||||
}, [panels, pluginPanelSet]);
|
_grouped_panels.push({
|
||||||
|
id: 'plugins',
|
||||||
|
label: markCustomPanels ? t`Plugin Provided` : '',
|
||||||
|
panels: pluginPanels
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [_allpanels, _grouped_panels];
|
||||||
|
}, [groups, panels, pluginPanelSet]);
|
||||||
|
|
||||||
const activePanels = useMemo(
|
const activePanels = useMemo(
|
||||||
() => allPanels.filter((panel) => !panel.hidden && !panel.disabled),
|
() => allPanels.filter((panel) => !panel.hidden && !panel.disabled),
|
||||||
@ -170,7 +215,17 @@ function BasePanelGroup({
|
|||||||
classNames={{ tab: classes.selectedPanelTab }}
|
classNames={{ tab: classes.selectedPanelTab }}
|
||||||
>
|
>
|
||||||
<Tabs.List justify='left' aria-label={`panel-tabs-${pageKey}`}>
|
<Tabs.List justify='left' aria-label={`panel-tabs-${pageKey}`}>
|
||||||
{allPanels.map(
|
{groupedPanels.map((group) => (
|
||||||
|
<Box key={`group-${group.id}`} w={'100%'}>
|
||||||
|
<Text
|
||||||
|
fs={'italic'}
|
||||||
|
ml={'1rem'}
|
||||||
|
c={vars.colors.primaryColors[7]}
|
||||||
|
key={`group-label-${group.id}`}
|
||||||
|
>
|
||||||
|
{group.label}
|
||||||
|
</Text>
|
||||||
|
{group.panels?.map(
|
||||||
(panel) =>
|
(panel) =>
|
||||||
!panel.hidden && (
|
!panel.hidden && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -182,11 +237,14 @@ function BasePanelGroup({
|
|||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
p='xs'
|
p='xs'
|
||||||
key={`panel-label-${panel.name}`}
|
key={`panel-label-${panel.name}`}
|
||||||
|
w={'100%'}
|
||||||
value={panel.name}
|
value={panel.name}
|
||||||
leftSection={panel.icon}
|
leftSection={panel.icon}
|
||||||
hidden={panel.hidden}
|
hidden={panel.hidden}
|
||||||
disabled={panel.disabled}
|
disabled={panel.disabled}
|
||||||
style={{ cursor: panel.disabled ? 'unset' : 'pointer' }}
|
style={{
|
||||||
|
cursor: panel.disabled ? 'unset' : 'pointer'
|
||||||
|
}}
|
||||||
onClick={(event: any) =>
|
onClick={(event: any) =>
|
||||||
handlePanelChange(panel.name, event)
|
handlePanelChange(panel.name, event)
|
||||||
}
|
}
|
||||||
@ -196,6 +254,8 @@ function BasePanelGroup({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
{collapsible && (
|
{collapsible && (
|
||||||
<Group wrap='nowrap' gap='xs'>
|
<Group wrap='nowrap' gap='xs'>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
|
@ -25,7 +25,10 @@ import { UserRoles } from '@lib/enums/Roles';
|
|||||||
import PermissionDenied from '../../../../components/errors/PermissionDenied';
|
import PermissionDenied from '../../../../components/errors/PermissionDenied';
|
||||||
import PageTitle from '../../../../components/nav/PageTitle';
|
import PageTitle from '../../../../components/nav/PageTitle';
|
||||||
import { SettingsHeader } from '../../../../components/nav/SettingsHeader';
|
import { SettingsHeader } from '../../../../components/nav/SettingsHeader';
|
||||||
import type { PanelType } from '../../../../components/panels/Panel';
|
import type {
|
||||||
|
PanelGroupType,
|
||||||
|
PanelType
|
||||||
|
} from '../../../../components/panels/Panel';
|
||||||
import { PanelGroup } from '../../../../components/panels/PanelGroup';
|
import { PanelGroup } from '../../../../components/panels/PanelGroup';
|
||||||
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
||||||
import { Loadable } from '../../../../functions/loading';
|
import { Loadable } from '../../../../functions/loading';
|
||||||
@ -104,7 +107,7 @@ export default function AdminCenter() {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'user',
|
name: 'user',
|
||||||
label: t`User Management`,
|
label: t`Users / Access`,
|
||||||
icon: <IconUsersGroup />,
|
icon: <IconUsersGroup />,
|
||||||
content: <UserManagementPanel />,
|
content: <UserManagementPanel />,
|
||||||
hidden: !user.hasViewRole(UserRoles.admin)
|
hidden: !user.hasViewRole(UserRoles.admin)
|
||||||
@ -224,6 +227,52 @@ export default function AdminCenter() {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
const grouping: PanelGroupType[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'ops',
|
||||||
|
label: t`Operations`,
|
||||||
|
panelIDs: [
|
||||||
|
'user',
|
||||||
|
'barcode-history',
|
||||||
|
'background',
|
||||||
|
'errors',
|
||||||
|
'currencies'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'data',
|
||||||
|
label: t`Data Management`,
|
||||||
|
panelIDs: [
|
||||||
|
'import',
|
||||||
|
'export',
|
||||||
|
'project-codes',
|
||||||
|
'custom-states',
|
||||||
|
'custom-units'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reporting',
|
||||||
|
label: t`Reporting`,
|
||||||
|
panelIDs: ['labels', 'reports']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'extend',
|
||||||
|
label: t`Extend / Integrate`,
|
||||||
|
panelIDs: ['plugin', 'machine']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'plm',
|
||||||
|
label: t`PLM`,
|
||||||
|
panelIDs: [
|
||||||
|
'part-parameters',
|
||||||
|
'category-parameters',
|
||||||
|
'location-types',
|
||||||
|
'stocktake'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -238,7 +287,9 @@ export default function AdminCenter() {
|
|||||||
<PanelGroup
|
<PanelGroup
|
||||||
pageKey='admin-center'
|
pageKey='admin-center'
|
||||||
panels={adminCenterPanels}
|
panels={adminCenterPanels}
|
||||||
|
groups={grouping}
|
||||||
collapsible={true}
|
collapsible={true}
|
||||||
|
markCustomPanels={true}
|
||||||
model='admincenter'
|
model='admincenter'
|
||||||
id={null}
|
id={null}
|
||||||
/>
|
/>
|
||||||
|
@ -21,7 +21,7 @@ test('Permissions - Admin', async ({ browser, request }) => {
|
|||||||
// Check for expected tabs
|
// Check for expected tabs
|
||||||
await loadTab(page, 'Machines');
|
await loadTab(page, 'Machines');
|
||||||
await loadTab(page, 'Plugins');
|
await loadTab(page, 'Plugins');
|
||||||
await loadTab(page, 'User Management');
|
await loadTab(page, 'Users / Access');
|
||||||
|
|
||||||
// Let's create a new user
|
// Let's create a new user
|
||||||
await page.getByLabel('action-button-add-user').click();
|
await page.getByLabel('action-button-add-user').click();
|
||||||
|
Reference in New Issue
Block a user