mirror of
https://github.com/inventree/InvenTree.git
synced 2026-10-14 14:11:55 +00:00
feat(frontend): optional nav tree sidebyside (#12974)
* disable restriction (for now) * add option for a permanent navigation tree * revert unintended change * adjust search text to be less confusing when pinned
This commit is contained in:
1 parent
47f2e6acdd
commit
600dc1a75b
8 files changed
+359
-242
No files matched your search
@@ -31,6 +31,7 @@ The *Display Settings* screen shows general display configuration options:
|
|||||||
{{ usersetting("SHOW_FULL_CATEGORY_IN_TABLES")}}
|
{{ usersetting("SHOW_FULL_CATEGORY_IN_TABLES")}}
|
||||||
{{ usersetting("SHOW_BOM_SUBASSEMBLY_LEVELS")}}
|
{{ usersetting("SHOW_BOM_SUBASSEMBLY_LEVELS")}}
|
||||||
{{ usersetting("ENABLE_LAST_BREADCRUMB") }}
|
{{ usersetting("ENABLE_LAST_BREADCRUMB") }}
|
||||||
|
{{ usersetting("NAVIGATION_TREE_PINNED") }}
|
||||||
{{ usersetting("SHOW_EXTRA_MODEL_INFO") }}
|
{{ usersetting("SHOW_EXTRA_MODEL_INFO") }}
|
||||||
{{ usersetting("SHOW_FULL_LOCATION_IN_TABLES") }}
|
{{ usersetting("SHOW_FULL_LOCATION_IN_TABLES") }}
|
||||||
{{ usersetting("DISPLAY_ITEMS_FINAL_LEVEL") }}
|
{{ usersetting("DISPLAY_ITEMS_FINAL_LEVEL") }}
|
||||||
|
|||||||
@@ -255,6 +255,12 @@ USER_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
|||||||
'default': False,
|
'default': False,
|
||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
'NAVIGATION_TREE_PINNED': {
|
||||||
|
'name': _('Pin Navigation Tree'),
|
||||||
|
'description': _('Display navigation tree permanently'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
'SHOW_EXTRA_MODEL_INFO': {
|
'SHOW_EXTRA_MODEL_INFO': {
|
||||||
'name': _('Show Extra Model Information'),
|
'name': _('Show Extra Model Information'),
|
||||||
'description': _('Display extra information in model selection dropdowns'),
|
'description': _('Display extra information in model selection dropdowns'),
|
||||||
|
|||||||
@@ -8,30 +8,41 @@ import {
|
|||||||
HoverCard,
|
HoverCard,
|
||||||
Loader,
|
Loader,
|
||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
|
Paper,
|
||||||
type RenderTreeNodePayload,
|
type RenderTreeNodePayload,
|
||||||
Space,
|
Space,
|
||||||
|
Splitter,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
|
Tooltip,
|
||||||
Tree,
|
Tree,
|
||||||
type TreeNodeData,
|
type TreeNodeData,
|
||||||
useTree
|
useTree
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useDebouncedValue } from '@mantine/hooks';
|
import { useDebouncedValue, useMediaQuery } from '@mantine/hooks';
|
||||||
import {
|
import {
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
IconChevronRight,
|
IconChevronRight,
|
||||||
IconExclamationCircle,
|
IconExclamationCircle,
|
||||||
|
IconPin,
|
||||||
|
IconPinnedOff,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconSitemap,
|
IconSitemap,
|
||||||
IconX
|
IconX
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import {
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState
|
||||||
|
} from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { StylishText } from '@lib/components/StylishText';
|
import { StylishText } from '@lib/components/StylishText';
|
||||||
import type { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import type { ModelType } from '@lib/enums/ModelType';
|
import type { ModelType } from '@lib/enums/ModelType';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import { resolveItem } from '@lib/functions/Conversion';
|
import { resolveItem } from '@lib/functions/Conversion';
|
||||||
@@ -42,6 +53,7 @@ import {
|
|||||||
} from '@lib/functions/Navigation';
|
} from '@lib/functions/Navigation';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { useApi } from '../../contexts/ApiContext';
|
import { useApi } from '../../contexts/ApiContext';
|
||||||
|
import { useUserSettingsState } from '../../states/SettingsStates';
|
||||||
import { ApiIcon } from '../items/ApiIcon';
|
import { ApiIcon } from '../items/ApiIcon';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -54,7 +66,9 @@ export default function NavigationTree({
|
|||||||
selectedId,
|
selectedId,
|
||||||
modelType,
|
modelType,
|
||||||
childIdentifier,
|
childIdentifier,
|
||||||
endpoint
|
endpoint,
|
||||||
|
hidden,
|
||||||
|
children
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
title: string;
|
title: string;
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
@@ -63,11 +77,39 @@ export default function NavigationTree({
|
|||||||
modelType: ModelType;
|
modelType: ModelType;
|
||||||
childIdentifier?: string;
|
childIdentifier?: string;
|
||||||
endpoint: ApiEndpoints;
|
endpoint: ApiEndpoints;
|
||||||
|
hidden?: boolean;
|
||||||
|
children?: ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const treeState = useTree();
|
const treeState = useTree();
|
||||||
|
|
||||||
|
const userSettings = useUserSettingsState();
|
||||||
|
const canPin = useMediaQuery('(min-width: 350px)', true, {
|
||||||
|
getInitialValueInEffect: false
|
||||||
|
});
|
||||||
|
const pinned = canPin && userSettings.isSet('NAVIGATION_TREE_PINNED', false);
|
||||||
|
const visible = !hidden && (pinned || opened);
|
||||||
|
|
||||||
|
const setPinned = useCallback(
|
||||||
|
(value: boolean) => {
|
||||||
|
api
|
||||||
|
.patch(
|
||||||
|
apiUrl(ApiEndpoints.settings_user_list, 'NAVIGATION_TREE_PINNED'),
|
||||||
|
{
|
||||||
|
value: value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
if (value) onClose();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
userSettings.fetchSettings();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[api, onClose, userSettings.fetchSettings]
|
||||||
|
);
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const [debouncedSearch] = useDebouncedValue(searchValue, 300);
|
const [debouncedSearch] = useDebouncedValue(searchValue, 300);
|
||||||
|
|
||||||
@@ -76,17 +118,17 @@ export default function NavigationTree({
|
|||||||
// PKs of nodes whose children are currently being fetched
|
// PKs of nodes whose children are currently being fetched
|
||||||
const [loadingNodes, setLoadingNodes] = useState<Set<number>>(new Set());
|
const [loadingNodes, setLoadingNodes] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
// Reset everything when the drawer opens or closes
|
// Reset everything when the tree is shown or hidden
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSearchValue('');
|
setSearchValue('');
|
||||||
setAllNodes([]);
|
setAllNodes([]);
|
||||||
setLoadingNodes(new Set());
|
setLoadingNodes(new Set());
|
||||||
}, [opened]);
|
}, [visible]);
|
||||||
|
|
||||||
// Data query — browse mode loads root nodes only; search mode loads all matches + ancestors
|
// Data query — browse mode loads root nodes only; search mode loads all matches + ancestors
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
enabled: opened,
|
enabled: visible,
|
||||||
queryKey: [modelType, 'tree', opened, debouncedSearch, selectedId],
|
queryKey: [modelType, 'tree', visible, debouncedSearch, selectedId],
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
api
|
api
|
||||||
.get(apiUrl(endpoint), {
|
.get(apiUrl(endpoint), {
|
||||||
@@ -350,68 +392,122 @@ export default function NavigationTree({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const header = (
|
||||||
<Drawer
|
<Group justify='space-between' wrap='nowrap' w='100%'>
|
||||||
opened={opened}
|
<Group justify='left' p='ms' gap='md' wrap='nowrap'>
|
||||||
size='lg'
|
<IconSitemap />
|
||||||
position='left'
|
<StylishText size='lg'>{title}</StylishText>
|
||||||
onClose={onClose}
|
</Group>
|
||||||
withCloseButton={true}
|
{canPin && (
|
||||||
styles={{
|
<Tooltip label={pinned ? t`Unpin` : t`Pin`} position='bottom'>
|
||||||
header: {
|
<ActionIcon
|
||||||
width: '100%'
|
variant='transparent'
|
||||||
},
|
aria-label={pinned ? 'nav-tree-unpin' : 'nav-tree-pin'}
|
||||||
title: {
|
onClick={() => setPinned(!pinned)}
|
||||||
width: '100%'
|
>
|
||||||
|
{pinned ? <IconPinnedOff /> : <IconPin />}
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<Stack gap='xs' pos='relative'>
|
||||||
|
<TextInput
|
||||||
|
aria-label='nav-tree-search'
|
||||||
|
placeholder={t`Filter ${{ title }}...`}
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(event) => setSearchValue(event.currentTarget.value)}
|
||||||
|
leftSection={<IconSearch size={16} />}
|
||||||
|
rightSection={
|
||||||
|
searchValue ? (
|
||||||
|
<ActionIcon
|
||||||
|
size='sm'
|
||||||
|
variant='transparent'
|
||||||
|
onClick={() => setSearchValue('')}
|
||||||
|
aria-label={t`Clear search`}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
}}
|
/>
|
||||||
title={
|
<Divider />
|
||||||
<Group justify='left' p='ms' gap='md' wrap='nowrap'>
|
<LoadingOverlay visible={query.isFetching || query.isLoading} />
|
||||||
<IconSitemap />
|
{query.isError ? (
|
||||||
<StylishText size='lg'>{title}</StylishText>
|
<Alert color='red' title={t`Error`} icon={<IconExclamationCircle />}>
|
||||||
</Group>
|
{t`Error loading navigation tree.`}
|
||||||
}
|
</Alert>
|
||||||
>
|
) : !query.isFetching && !query.isLoading && data.length === 0 ? (
|
||||||
<Stack gap='xs'>
|
<Alert color='blue' icon={<IconSearch />}>
|
||||||
<TextInput
|
{t`No results found`}
|
||||||
aria-label='nav-tree-search'
|
</Alert>
|
||||||
placeholder={t`Search...`}
|
) : (
|
||||||
value={searchValue}
|
<Tree
|
||||||
onChange={(event) => setSearchValue(event.currentTarget.value)}
|
data={data}
|
||||||
leftSection={<IconSearch size={16} />}
|
tree={treeState}
|
||||||
rightSection={
|
renderNode={renderNode}
|
||||||
searchValue ? (
|
withLines
|
||||||
<ActionIcon
|
levelOffset={25}
|
||||||
size='sm'
|
|
||||||
variant='transparent'
|
|
||||||
onClick={() => setSearchValue('')}
|
|
||||||
aria-label={t`Clear search`}
|
|
||||||
>
|
|
||||||
<IconX size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Divider />
|
)}
|
||||||
<LoadingOverlay visible={query.isFetching || query.isLoading} />
|
</Stack>
|
||||||
{query.isError ? (
|
);
|
||||||
<Alert color='red' title={t`Error`} icon={<IconExclamationCircle />}>
|
|
||||||
{t`Error loading navigation tree.`}
|
if (hidden) {
|
||||||
</Alert>
|
return <>{children}</>;
|
||||||
) : !query.isFetching && !query.isLoading && data.length === 0 ? (
|
}
|
||||||
<Alert color='blue' icon={<IconSearch />}>
|
|
||||||
{t`No results found`}
|
if (pinned) {
|
||||||
</Alert>
|
return (
|
||||||
) : (
|
<Splitter
|
||||||
<Tree
|
onCollapseChange={(event) => {
|
||||||
data={data}
|
console.log(event);
|
||||||
tree={treeState}
|
setPinned(!pinned);
|
||||||
renderNode={renderNode}
|
}}
|
||||||
withLines
|
>
|
||||||
levelOffset={25}
|
<Splitter.Pane defaultSize='350px' min={'200px'} collapsible>
|
||||||
/>
|
<Paper
|
||||||
)}
|
withBorder
|
||||||
</Stack>
|
p='xs'
|
||||||
</Drawer>
|
aria-label='nav-tree-pinned'
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
<Stack gap='xs'>
|
||||||
|
{header}
|
||||||
|
{content}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Splitter.Pane>
|
||||||
|
<Splitter.Pane defaultSize={50} min={20}>
|
||||||
|
{children}
|
||||||
|
</Splitter.Pane>
|
||||||
|
</Splitter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Drawer
|
||||||
|
opened={opened}
|
||||||
|
size='lg'
|
||||||
|
position='left'
|
||||||
|
onClose={onClose}
|
||||||
|
withCloseButton={true}
|
||||||
|
styles={{
|
||||||
|
header: {
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
width: '100%'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={header}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Drawer>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -63,6 +63,7 @@ export default function UserSettings() {
|
|||||||
'FORMS_CLOSE_USING_ESCAPE',
|
'FORMS_CLOSE_USING_ESCAPE',
|
||||||
'DISPLAY_STOCKTAKE_TAB',
|
'DISPLAY_STOCKTAKE_TAB',
|
||||||
'ENABLE_LAST_BREADCRUMB',
|
'ENABLE_LAST_BREADCRUMB',
|
||||||
|
'NAVIGATION_TREE_PINNED',
|
||||||
'SHOW_EXTRA_MODEL_INFO',
|
'SHOW_EXTRA_MODEL_INFO',
|
||||||
'SHOW_FULL_LOCATION_IN_TABLES',
|
'SHOW_FULL_LOCATION_IN_TABLES',
|
||||||
'SHOW_FULL_CATEGORY_IN_TABLES',
|
'SHOW_FULL_CATEGORY_IN_TABLES',
|
||||||
|
|||||||
@@ -318,30 +318,34 @@ export default function CategoryDetail() {
|
|||||||
setTreeOpen(false);
|
setTreeOpen(false);
|
||||||
}}
|
}}
|
||||||
selectedId={category?.pk}
|
selectedId={category?.pk}
|
||||||
/>
|
>
|
||||||
<PageDetail
|
<Stack gap='xs'>
|
||||||
title={(category?.name ?? id) ? t`Part Category` : t`Parts`}
|
<PageDetail
|
||||||
subtitle={category?.description}
|
title={(category?.name ?? id) ? t`Part Category` : t`Parts`}
|
||||||
icon={category?.icon && <ApiIcon name={category?.icon} />}
|
subtitle={category?.description}
|
||||||
breadcrumbs={breadcrumbs}
|
icon={category?.icon && <ApiIcon name={category?.icon} />}
|
||||||
breadcrumbAction={() => {
|
breadcrumbs={breadcrumbs}
|
||||||
setTreeOpen(true);
|
breadcrumbAction={() => {
|
||||||
}}
|
setTreeOpen(true);
|
||||||
actions={categoryActions}
|
}}
|
||||||
editAction={editCategory.open}
|
actions={categoryActions}
|
||||||
editEnabled={
|
editAction={editCategory.open}
|
||||||
!!category?.pk && user.hasChangePermission(ModelType.partcategory)
|
editEnabled={
|
||||||
}
|
!!category?.pk &&
|
||||||
/>
|
user.hasChangePermission(ModelType.partcategory)
|
||||||
<PanelGroup
|
}
|
||||||
pageKey='partcategory'
|
/>
|
||||||
panels={panels}
|
<PanelGroup
|
||||||
model={ModelType.partcategory}
|
pageKey='partcategory'
|
||||||
instance={category}
|
panels={panels}
|
||||||
reloadInstance={refreshInstance}
|
model={ModelType.partcategory}
|
||||||
id={category.pk ?? null}
|
instance={category}
|
||||||
defaultPanel={defaultPanel}
|
reloadInstance={refreshInstance}
|
||||||
/>
|
id={category.pk ?? null}
|
||||||
|
defaultPanel={defaultPanel}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</NavigationTree>
|
||||||
</Stack>
|
</Stack>
|
||||||
</InstanceDetail>
|
</InstanceDetail>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -797,83 +797,87 @@ export default function PartDetail() {
|
|||||||
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
||||||
<InstanceDetail query={instanceQuery} requiredRole={UserRoles.part}>
|
<InstanceDetail query={instanceQuery} requiredRole={UserRoles.part}>
|
||||||
<Stack gap='xs'>
|
<Stack gap='xs'>
|
||||||
{user.hasViewRole(UserRoles.part_category) && (
|
<NavigationTree
|
||||||
<NavigationTree
|
hidden={!user.hasViewRole(UserRoles.part_category)}
|
||||||
title={t`Part Categories`}
|
title={t`Part Categories`}
|
||||||
childIdentifier='subcategories'
|
childIdentifier='subcategories'
|
||||||
modelType={ModelType.partcategory}
|
modelType={ModelType.partcategory}
|
||||||
endpoint={ApiEndpoints.category_tree}
|
endpoint={ApiEndpoints.category_tree}
|
||||||
opened={treeOpen}
|
opened={treeOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setTreeOpen(false);
|
setTreeOpen(false);
|
||||||
}}
|
|
||||||
selectedId={part?.category}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<PageDetail
|
|
||||||
title={`${t`Part`}: ${part.full_name}`}
|
|
||||||
icon={
|
|
||||||
lockingEnabled ? (
|
|
||||||
<ActionIcon
|
|
||||||
aria-label='part-lock-icon'
|
|
||||||
variant='transparent'
|
|
||||||
disabled={!user.hasChangeRole(UserRoles.part)}
|
|
||||||
onClick={() => {
|
|
||||||
const locking = !part.locked;
|
|
||||||
api
|
|
||||||
.patch(apiUrl(ApiEndpoints.part_list, part.pk), {
|
|
||||||
locked: locking
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
notifications.hide('part-lock');
|
|
||||||
notifications.show({
|
|
||||||
id: 'part-lock',
|
|
||||||
message: locking ? t`Part locked` : t`Part unlocked`,
|
|
||||||
color: 'green',
|
|
||||||
icon: locking ? (
|
|
||||||
<IconLock size='1rem' />
|
|
||||||
) : (
|
|
||||||
<IconLockOpen size='1rem' />
|
|
||||||
)
|
|
||||||
});
|
|
||||||
refreshInstance();
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{part?.locked ? <IconLock /> : <IconLockOpen />}
|
|
||||||
</ActionIcon>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
subtitle={part.description}
|
|
||||||
imageUrl={part.image}
|
|
||||||
thumbnailUrl={part.thumbnail}
|
|
||||||
badges={badges}
|
|
||||||
breadcrumbs={
|
|
||||||
user.hasViewRole(UserRoles.part_category)
|
|
||||||
? breadcrumbs
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
lastCrumb={[
|
|
||||||
{
|
|
||||||
name: part.name,
|
|
||||||
url: `/part/${part.pk}/`
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
breadcrumbAction={() => {
|
|
||||||
setTreeOpen(true);
|
|
||||||
}}
|
}}
|
||||||
editAction={editPart.open}
|
selectedId={part?.category}
|
||||||
editEnabled={user.hasChangeRole(UserRoles.part)}
|
>
|
||||||
actions={partActions}
|
<Stack gap='xs'>
|
||||||
/>
|
<PageDetail
|
||||||
<PanelGroup
|
title={`${t`Part`}: ${part.full_name}`}
|
||||||
pageKey='part'
|
icon={
|
||||||
panels={partPanels}
|
lockingEnabled ? (
|
||||||
instance={part}
|
<ActionIcon
|
||||||
reloadInstance={refreshInstance}
|
aria-label='part-lock-icon'
|
||||||
model={ModelType.part}
|
variant='transparent'
|
||||||
id={part.pk}
|
disabled={!user.hasChangeRole(UserRoles.part)}
|
||||||
/>
|
onClick={() => {
|
||||||
|
const locking = !part.locked;
|
||||||
|
api
|
||||||
|
.patch(apiUrl(ApiEndpoints.part_list, part.pk), {
|
||||||
|
locked: locking
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
notifications.hide('part-lock');
|
||||||
|
notifications.show({
|
||||||
|
id: 'part-lock',
|
||||||
|
message: locking
|
||||||
|
? t`Part locked`
|
||||||
|
: t`Part unlocked`,
|
||||||
|
color: 'green',
|
||||||
|
icon: locking ? (
|
||||||
|
<IconLock size='1rem' />
|
||||||
|
) : (
|
||||||
|
<IconLockOpen size='1rem' />
|
||||||
|
)
|
||||||
|
});
|
||||||
|
refreshInstance();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{part?.locked ? <IconLock /> : <IconLockOpen />}
|
||||||
|
</ActionIcon>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
subtitle={part.description}
|
||||||
|
imageUrl={part.image}
|
||||||
|
thumbnailUrl={part.thumbnail}
|
||||||
|
badges={badges}
|
||||||
|
breadcrumbs={
|
||||||
|
user.hasViewRole(UserRoles.part_category)
|
||||||
|
? breadcrumbs
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
lastCrumb={[
|
||||||
|
{
|
||||||
|
name: part.name,
|
||||||
|
url: `/part/${part.pk}/`
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
breadcrumbAction={() => {
|
||||||
|
setTreeOpen(true);
|
||||||
|
}}
|
||||||
|
editAction={editPart.open}
|
||||||
|
editEnabled={user.hasChangeRole(UserRoles.part)}
|
||||||
|
actions={partActions}
|
||||||
|
/>
|
||||||
|
<PanelGroup
|
||||||
|
pageKey='part'
|
||||||
|
panels={partPanels}
|
||||||
|
instance={part}
|
||||||
|
reloadInstance={refreshInstance}
|
||||||
|
model={ModelType.part}
|
||||||
|
id={part.pk}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</NavigationTree>
|
||||||
</Stack>
|
</Stack>
|
||||||
</InstanceDetail>
|
</InstanceDetail>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -438,38 +438,41 @@ export default function Stock() {
|
|||||||
opened={treeOpen}
|
opened={treeOpen}
|
||||||
onClose={() => setTreeOpen(false)}
|
onClose={() => setTreeOpen(false)}
|
||||||
selectedId={location?.pk}
|
selectedId={location?.pk}
|
||||||
/>
|
>
|
||||||
<PageDetail
|
<Stack>
|
||||||
title={(location?.name ?? id) ? t`Stock Location` : t`Stock`}
|
<PageDetail
|
||||||
subtitle={location?.description}
|
title={(location?.name ?? id) ? t`Stock Location` : t`Stock`}
|
||||||
icon={location?.icon && <ApiIcon name={location?.icon} />}
|
subtitle={location?.description}
|
||||||
actions={location?.pk ? locationActions : undefined}
|
icon={location?.icon && <ApiIcon name={location?.icon} />}
|
||||||
editAction={editLocation.open}
|
actions={location?.pk ? locationActions : undefined}
|
||||||
editEnabled={
|
editAction={editLocation.open}
|
||||||
!!location?.pk &&
|
editEnabled={
|
||||||
user.hasChangePermission(ModelType.stocklocation)
|
!!location?.pk &&
|
||||||
}
|
user.hasChangePermission(ModelType.stocklocation)
|
||||||
breadcrumbs={breadcrumbs}
|
}
|
||||||
lastCrumb={[
|
breadcrumbs={breadcrumbs}
|
||||||
{
|
lastCrumb={[
|
||||||
name: location.name,
|
{
|
||||||
url: `/stock/location/${location.pk}/`
|
name: location.name,
|
||||||
}
|
url: `/stock/location/${location.pk}/`
|
||||||
]}
|
}
|
||||||
breadcrumbAction={() => {
|
]}
|
||||||
setTreeOpen(true);
|
breadcrumbAction={() => {
|
||||||
}}
|
setTreeOpen(true);
|
||||||
/>
|
}}
|
||||||
<PanelGroup
|
/>
|
||||||
pageKey='stocklocation'
|
<PanelGroup
|
||||||
panels={locationPanels}
|
pageKey='stocklocation'
|
||||||
model={ModelType.stocklocation}
|
panels={locationPanels}
|
||||||
reloadInstance={refreshInstance}
|
model={ModelType.stocklocation}
|
||||||
id={location?.pk}
|
reloadInstance={refreshInstance}
|
||||||
instance={location}
|
id={location?.pk}
|
||||||
pluginPanelWithoutId
|
instance={location}
|
||||||
defaultPanel={defaultPanel}
|
pluginPanelWithoutId
|
||||||
/>
|
defaultPanel={defaultPanel}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</NavigationTree>
|
||||||
</Stack>
|
</Stack>
|
||||||
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
{stockAdjustActions.modals.map((modal) => modal.modal)}
|
||||||
</InstanceDetail>
|
</InstanceDetail>
|
||||||
|
|||||||
@@ -728,47 +728,49 @@ export default function StockDetail() {
|
|||||||
requiredPermission={ModelType.stockitem}
|
requiredPermission={ModelType.stockitem}
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
{user.hasViewRole(UserRoles.stock_location) && (
|
<NavigationTree
|
||||||
<NavigationTree
|
hidden={!user.hasViewRole(UserRoles.stock_location)}
|
||||||
title={t`Stock Locations`}
|
title={t`Stock Locations`}
|
||||||
modelType={ModelType.stocklocation}
|
modelType={ModelType.stocklocation}
|
||||||
endpoint={ApiEndpoints.stock_location_tree}
|
endpoint={ApiEndpoints.stock_location_tree}
|
||||||
childIdentifier='sublocations'
|
childIdentifier='sublocations'
|
||||||
opened={treeOpen}
|
opened={treeOpen}
|
||||||
onClose={() => setTreeOpen(false)}
|
onClose={() => setTreeOpen(false)}
|
||||||
selectedId={stockitem?.location}
|
selectedId={stockitem?.location}
|
||||||
/>
|
>
|
||||||
)}
|
<Stack>
|
||||||
<PageDetail
|
<PageDetail
|
||||||
title={t`Stock Item`}
|
title={t`Stock Item`}
|
||||||
subtitle={stockitem.part_detail?.full_name}
|
subtitle={stockitem.part_detail?.full_name}
|
||||||
imageUrl={stockitem.part_detail?.thumbnail}
|
imageUrl={stockitem.part_detail?.thumbnail}
|
||||||
editAction={editStockItem.open}
|
editAction={editStockItem.open}
|
||||||
editEnabled={user.hasChangePermission(ModelType.stockitem)}
|
editEnabled={user.hasChangePermission(ModelType.stockitem)}
|
||||||
badges={stockBadges}
|
badges={stockBadges}
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
user.hasViewRole(UserRoles.stock_location)
|
user.hasViewRole(UserRoles.stock_location)
|
||||||
? breadcrumbs
|
? breadcrumbs
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
lastCrumb={[
|
lastCrumb={[
|
||||||
{
|
{
|
||||||
name: stockitem.name,
|
name: stockitem.name,
|
||||||
url: `/stock/item/${stockitem.pk}/`
|
url: `/stock/item/${stockitem.pk}/`
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
breadcrumbAction={() => {
|
breadcrumbAction={() => {
|
||||||
setTreeOpen(true);
|
setTreeOpen(true);
|
||||||
}}
|
}}
|
||||||
actions={stockActions}
|
actions={stockActions}
|
||||||
/>
|
/>
|
||||||
<PanelGroup
|
<PanelGroup
|
||||||
pageKey='stockitem'
|
pageKey='stockitem'
|
||||||
panels={stockPanels}
|
panels={stockPanels}
|
||||||
model={ModelType.stockitem}
|
model={ModelType.stockitem}
|
||||||
id={stockitem.pk}
|
id={stockitem.pk}
|
||||||
instance={stockitem}
|
instance={stockitem}
|
||||||
/>
|
/>
|
||||||
|
</Stack>
|
||||||
|
</NavigationTree>
|
||||||
</Stack>
|
</Stack>
|
||||||
</InstanceDetail>
|
</InstanceDetail>
|
||||||
{editStockItem.modal}
|
{editStockItem.modal}
|
||||||
|
|||||||
Reference in new issue
Block a user