[PUI] stock item delete (#7204)

* Handle stock item delete in PUI

* Support deletion of stock location

* Delete part category

* Some refactoring of the TableField approach

- Still needs some work
- Code can be made a lot cleaner here

* Use mantine components

* Fix incorrect import

* Update ServerInfoModal

* Further table refactoring

* Implement delete part function
This commit is contained in:
Oliver 2024-05-13 11:04:19 +10:00 committed by GitHub
parent 700a3612b7
commit b5a3e4aac4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 399 additions and 252 deletions

View File

@ -32,7 +32,7 @@ export function ChoiceField({
return choices.map((choice) => {
return {
value: choice.value.toString(),
label: choice.display_name.toString()
label: choice.display_name ?? choice.value
};
});
}, [definition.choices]);

View File

@ -1,5 +1,5 @@
import { Trans, t } from '@lingui/macro';
import { Table } from '@mantine/core';
import { Container, Flex, Group, Table } from '@mantine/core';
import { FieldValues, UseControllerReturn } from 'react-hook-form';
import { InvenTreeIcon } from '../../../functions/icons';
@ -34,19 +34,21 @@ export function TableField({
return (
<Table highlightOnHover striped>
<thead>
<tr>
<Table.Thead>
<Table.Tr>
{definition.headers?.map((header) => {
return <th key={header}>{header}</th>;
})}
</tr>
</thead>
<tbody>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{value.length > 0 ? (
value.map((item: any, idx: number) => {
// Table fields require render function
if (!definition.modelRenderer) {
return <tr>{t`modelRenderer entry required for tables`}</tr>;
return (
<Table.Tr>{t`modelRenderer entry required for tables`}</Table.Tr>
);
}
return definition.modelRenderer({
item: item,
@ -56,8 +58,8 @@ export function TableField({
});
})
) : (
<tr>
<td
<Table.Tr>
<Table.Td
style={{ textAlign: 'center' }}
colSpan={definition.headers?.length}
>
@ -71,10 +73,36 @@ export function TableField({
<InvenTreeIcon icon="info" />
<Trans>No entries available</Trans>
</span>
</td>
</tr>
</Table.Td>
</Table.Tr>
)}
</tbody>
</Table.Tbody>
</Table>
);
}
/*
* Display an "extra" row below the main table row, for additional information.
*/
export function TableFieldExtraRow({
visible,
content,
colSpan
}: {
visible: boolean;
content: React.ReactNode;
colSpan?: number;
}) {
return (
visible && (
<Table.Tr>
<Table.Td colSpan={colSpan ?? 3}>
<Group justify="flex-start" grow>
<InvenTreeIcon icon="downright" />
{content}
</Group>
</Table.Td>
</Table.Tr>
)
);
}

View File

@ -189,10 +189,12 @@ export function EditItemAction({
// Common action button for deleting an item
export function DeleteItemAction({
hidden = false,
disabled = false,
tooltip,
onClick
}: {
hidden?: boolean;
disabled?: boolean;
tooltip?: string;
onClick?: () => void;
}): ActionDropdownItem {
@ -201,7 +203,8 @@ export function DeleteItemAction({
name: t`Delete`,
tooltip: tooltip ?? t`Delete item`,
onClick: onClick,
hidden: hidden
hidden: hidden,
disabled: disabled
};
}

View File

@ -26,46 +26,46 @@ export function ServerInfoModal({
<Trans>Server</Trans>
</Title>
<Table>
<tbody>
<tr>
<td>
<Table.Tbody>
<Table.Tr>
<Table.Td>
<Trans>Instance Name</Trans>
</td>
<td>{server.instance}</td>
</tr>
<tr>
<td>
</Table.Td>
<Table.Td>{server.instance}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Database</Trans>
</td>
<td>
</Table.Td>
<Table.Td>
<OnlyStaff>{server.database}</OnlyStaff>
</td>
</tr>
</Table.Td>
</Table.Tr>
{server.debug_mode && (
<tr>
<td>
<Table.Tr>
<Table.Td>
<Trans>Debug Mode</Trans>
</td>
<td>
</Table.Td>
<Table.Td>
<Trans>Server is running in debug mode</Trans>
</td>
</tr>
</Table.Td>
</Table.Tr>
)}
{server.docker_mode && (
<tr>
<td>
<Table.Tr>
<Table.Td>
<Trans>Docker Mode</Trans>
</td>
<td>
</Table.Td>
<Table.Td>
<Trans>Server is deployed using docker</Trans>
</td>
</tr>
</Table.Td>
</Table.Tr>
)}
<tr>
<td>
<Table.Tr>
<Table.Td>
<Trans>Plugin Support</Trans>
</td>
<td>
</Table.Td>
<Table.Td>
<Badge color={server.plugins_enabled ? 'green' : 'red'}>
{server.plugins_enabled ? (
<Trans>Plugin support enabled</Trans>
@ -73,13 +73,13 @@ export function ServerInfoModal({
<Trans>Plugin support disabled</Trans>
)}
</Badge>
</td>
</tr>
<tr>
<td>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Server status</Trans>
</td>
<td>
</Table.Td>
<Table.Td>
<OnlyStaff>
<Badge color={server.system_health ? 'green' : 'yellow'}>
{server.system_health ? (
@ -89,52 +89,52 @@ export function ServerInfoModal({
)}
</Badge>
</OnlyStaff>
</td>
</tr>
</Table.Td>
</Table.Tr>
{server.worker_running != true && (
<tr>
<td>
<Table.Tr>
<Table.Td>
<Trans>Background Worker</Trans>
</td>
<td>
</Table.Td>
<Table.Td>
<Badge color="red">
<Trans>Background worker not running</Trans>
</Badge>
</td>
</tr>
</Table.Td>
</Table.Tr>
)}
{server.email_configured != true && (
<tr>
<td>
<Table.Tr>
<Table.Td>
<Trans>Email Settings</Trans>
</td>
<td>
</Table.Td>
<Table.Td>
<Badge color="red">
<Trans>Email settings not configured</Trans>
</Badge>
</td>
</tr>
</Table.Td>
</Table.Tr>
)}
</tbody>
</Table.Tbody>
</Table>
<Title order={5}>
<Trans>Version</Trans>
</Title>
<Table>
<tbody>
<tr>
<td>
<Table.Tbody>
<Table.Tr>
<Table.Td>
<Trans>Server Version</Trans>
</td>
<td>{server.version}</td>
</tr>
<tr>
<td>
</Table.Td>
<Table.Td>{server.version}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>API Version</Trans>
</td>
<td>{server.apiVersion}</td>
</tr>
</tbody>
</Table.Td>
<Table.Td>{server.apiVersion}</Table.Td>
</Table.Tr>
</Table.Tbody>
</Table>
<Divider />
<Group justify="right">

View File

@ -1,5 +1,12 @@
import { t } from '@lingui/macro';
import { Flex, FocusTrap, Modal, NumberInput, TextInput } from '@mantine/core';
import {
Flex,
FocusTrap,
Modal,
NumberInput,
Table,
TextInput
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import {
IconAddressBook,
@ -24,6 +31,7 @@ import {
ApiFormAdjustFilterType,
ApiFormFieldSet
} from '../components/forms/fields/ApiFormField';
import { TableFieldExtraRow } from '../components/forms/fields/TableField';
import { Thumbnail } from '../components/images/Thumbnail';
import { ProgressBar } from '../components/items/ProgressBar';
import { StylishText } from '../components/items/StylishText';
@ -308,8 +316,8 @@ function LineItemFormRow({
/>
</FocusTrap>
</Modal>
<tr>
<td>
<Table.Tr>
<Table.Td>
<Flex gap="sm" align="center">
<Thumbnail
size={40}
@ -318,16 +326,16 @@ function LineItemFormRow({
/>
<div>{record.part_detail.name}</div>
</Flex>
</td>
<td>{record.supplier_part_detail.SKU}</td>
<td>
</Table.Td>
<Table.Td>{record.supplier_part_detail.SKU}</Table.Td>
<Table.Td>
<ProgressBar
value={record.received}
maximum={record.quantity}
progressLabel
/>
</td>
<td style={{ width: '1%', whiteSpace: 'nowrap' }}>
</Table.Td>
<Table.Td style={{ width: '1%', whiteSpace: 'nowrap' }}>
<NumberInput
value={input.item.quantity}
style={{ width: '100px' }}
@ -335,8 +343,8 @@ function LineItemFormRow({
min={0}
onChange={(value) => input.changeFn(input.idx, 'quantity', value)}
/>
</td>
<td style={{ width: '1%', whiteSpace: 'nowrap' }}>
</Table.Td>
<Table.Td style={{ width: '1%', whiteSpace: 'nowrap' }}>
<Flex gap="1px">
<ActionButton
onClick={() => locationHandlers.toggle()}
@ -387,11 +395,11 @@ function LineItemFormRow({
color="red"
/>
</Flex>
</td>
</tr>
</Table.Td>
</Table.Tr>
{locationOpen && (
<tr>
<td colSpan={4}>
<Table.Tr>
<Table.Td colSpan={4}>
<Flex align="end" gap={5}>
<div style={{ flexGrow: '1' }}>
<StandaloneField
@ -453,8 +461,8 @@ function LineItemFormRow({
)}
</Flex>
</Flex>
</td>
<td>
</Table.Td>
<Table.Td>
<div
style={{
height: '100%',
@ -466,15 +474,13 @@ function LineItemFormRow({
>
<InvenTreeIcon icon="downleft" />
</div>
</td>
</tr>
</Table.Td>
</Table.Tr>
)}
{batchOpen && (
<>
<tr>
<td colSpan={4}>
<Flex align="end" gap={5}>
<div style={{ flexGrow: '1' }}>
<TableFieldExtraRow
visible={batchOpen}
colSpan={4}
content={
<StandaloneField
fieldDefinition={{
field_type: 'string',
@ -483,29 +489,12 @@ function LineItemFormRow({
value: batchCode
}}
/>
</div>
</Flex>
</td>
<td>
<div
style={{
height: '100%',
display: 'grid',
gridTemplateColumns: 'repeat(6, 1fr)',
gridTemplateRows: 'auto',
alignItems: 'end'
}}
>
<span></span>
<InvenTreeIcon icon="downleft" />
</div>
</td>
</tr>
{record.trackable && (
<tr>
<td colSpan={4}>
<Flex align="end" gap={5}>
<div style={{ flexGrow: '1' }}>
}
/>
<TableFieldExtraRow
visible={batchOpen && record.trackable}
colSpan={4}
content={
<StandaloneField
fieldDefinition={{
field_type: 'string',
@ -514,30 +503,12 @@ function LineItemFormRow({
value: serials
}}
/>
</div>
</Flex>
</td>
<td>
<div
style={{
height: '100%',
display: 'grid',
gridTemplateColumns: 'repeat(6, 1fr)',
gridTemplateRows: 'auto',
alignItems: 'end'
}}
>
<span></span>
<InvenTreeIcon icon="downleft" />
</div>
</td>
</tr>
)}
</>
)}
{statusOpen && (
<tr>
<td colSpan={4}>
}
/>
<TableFieldExtraRow
visible={statusOpen}
colSpan={4}
content={
<StandaloneField
fieldDefinition={{
field_type: 'choice',
@ -549,24 +520,8 @@ function LineItemFormRow({
}}
defaultValue={10}
/>
</td>
<td>
<div
style={{
height: '100%',
display: 'grid',
gridTemplateColumns: 'repeat(6, 1fr)',
gridTemplateRows: 'auto',
alignItems: 'end'
}}
>
<span></span>
<span></span>
<InvenTreeIcon icon="downleft" />
</div>
</td>
</tr>
)}
}
/>
</>
);
}
@ -655,11 +610,11 @@ export function useReceiveLineItems(props: LineItemsForm) {
return useCreateApiFormModal({
...props.formProps,
url: url,
title: t`Receive line items`,
title: t`Receive Line Items`,
fields: fields,
initialData: {
location: null
},
size: 'max(60%,800px)'
size: 'xl'
});
}

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
import { Flex, Group, NumberInput, Skeleton, Text } from '@mantine/core';
import { Flex, Group, NumberInput, Skeleton, Table, Text } from '@mantine/core';
import { modals } from '@mantine/modals';
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { Suspense, useCallback, useMemo, useState } from 'react';
@ -297,8 +297,8 @@ function StockOperationsRow({
return !record ? (
<div>{t`Loading...`}</div>
) : (
<tr>
<td>
<Table.Tr>
<Table.Td>
<Flex gap="sm" align="center">
<Thumbnail
size={40}
@ -307,18 +307,20 @@ function StockOperationsRow({
/>
<div>{record.part_detail?.name}</div>
</Flex>
</td>
<td>{record.location ? record.location_detail?.pathstring : '-'}</td>
<td>
</Table.Td>
<Table.Td>
{record.location ? record.location_detail?.pathstring : '-'}
</Table.Td>
<Table.Td>
<Flex align="center" gap="xs">
<Group justify="space-between">
<Text>{stockString}</Text>
<StatusRenderer status={record.status} type={ModelType.stockitem} />
</Group>
</Flex>
</td>
</Table.Td>
{!merge && (
<td>
<Table.Td>
<NumberInput
value={value}
onChange={onChange}
@ -327,9 +329,9 @@ function StockOperationsRow({
min={0}
style={{ maxWidth: '100px' }}
/>
</td>
</Table.Td>
)}
<td>
<Table.Td>
<Flex gap="3px">
{transfer && (
<ActionButton
@ -351,8 +353,8 @@ function StockOperationsRow({
color="red"
/>
</Flex>
</td>
</tr>
</Table.Td>
</Table.Tr>
);
}

View File

@ -22,6 +22,7 @@ import {
IconClipboardText,
IconCopy,
IconCornerDownLeft,
IconCornerDownRight,
IconCornerUpRightDouble,
IconCurrencyDollar,
IconDots,
@ -190,6 +191,7 @@ const icons = {
phone: IconPhone,
sitemap: IconSitemap,
downleft: IconCornerDownLeft,
downright: IconCornerDownRight,
barcode: IconQrcode,
barLine: IconMinusVertical,
batch_code: IconClipboardText,

View File

@ -81,41 +81,41 @@ export function UserTheme({ height }: { height: number }) {
<Trans>Theme</Trans>
</Title>
<Table>
<tbody>
<tr>
<td>
<Table.Tbody>
<Table.Tr>
<Table.Td>
<Trans>Primary color</Trans>
</td>
<td>
</Table.Td>
<Table.Td>
<ColorPicker
format="hex"
onChange={changePrimary}
withPicker={false}
swatches={Object.keys(LOOKUP)}
/>
</td>
</tr>
<tr>
<td>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>White color</Trans>
</td>
<td>
</Table.Td>
<Table.Td>
<ColorInput value={whiteColor} onChange={changeWhite} />
</td>
</tr>
<tr>
<td>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Black color</Trans>
</td>
<td>
</Table.Td>
<Table.Td>
<ColorInput value={blackColor} onChange={changeBlack} />
</td>
</tr>
<tr>
<td>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Border Radius</Trans>
</td>
<td>
</Table.Td>
<Table.Td>
<Slider
label={(val) => getMark(val).label}
defaultValue={50}
@ -125,13 +125,13 @@ export function UserTheme({ height }: { height: number }) {
onChange={changeRadius}
mb={18}
/>
</td>
</tr>
<tr>
<td>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Loader</Trans>
</td>
<td>
</Table.Td>
<Table.Td>
<Group align="center">
<Select
data={loaderDate}
@ -140,9 +140,9 @@ export function UserTheme({ height }: { height: number }) {
/>
<Loader type={themeLoader} mah={18} />
</Group>
</td>
</tr>
</tbody>
</Table.Td>
</Table.Tr>
</Table.Tbody>
</Table>
</Container>
);

View File

@ -8,12 +8,13 @@ import {
IconSitemap
} from '@tabler/icons-react';
import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
ActionDropdown,
DeleteItemAction,
EditItemAction
} from '../../components/items/ActionDropdown';
import { PageDetail } from '../../components/nav/PageDetail';
@ -24,7 +25,10 @@ import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { partCategoryFields } from '../../forms/PartForms';
import { getDetailUrl } from '../../functions/urls';
import { useEditApiFormModal } from '../../hooks/UseForm';
import {
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import { useUserState } from '../../states/UserState';
import ParametricPartTable from '../../tables/part/ParametricPartTable';
@ -43,6 +47,7 @@ export default function CategoryDetail({}: {}) {
[_id]
);
const navigate = useNavigate();
const user = useUserState();
const [treeOpen, setTreeOpen] = useState(false);
@ -154,6 +159,46 @@ export default function CategoryDetail({}: {}) {
onFormSuccess: refreshInstance
});
const deleteOptions = useMemo(() => {
return [
{
value: 0,
display_name: `Move items to parent category`
},
{
value: 1,
display_name: t`Delete items`
}
];
}, []);
const deleteCategory = useDeleteApiFormModal({
url: ApiEndpoints.category_list,
pk: id,
title: t`Delete Part Category`,
fields: {
delete_parts: {
label: t`Parts Action`,
description: t`Action for parts in this category`,
choices: deleteOptions,
field_type: 'choice'
},
delete_child_categories: {
label: t`Child Categories Action`,
description: t`Action for child categories in this category`,
choices: deleteOptions,
field_type: 'choice'
}
},
onFormSuccess: () => {
if (category.parent) {
navigate(getDetailUrl(ModelType.partcategory, category.parent));
} else {
navigate('/part/');
}
}
});
const categoryActions = useMemo(() => {
return [
<ActionDropdown
@ -165,6 +210,11 @@ export default function CategoryDetail({}: {}) {
hidden: !id || !user.hasChangeRole(UserRoles.part_category),
tooltip: t`Edit Part Category`,
onClick: () => editCategory.open()
}),
DeleteItemAction({
hidden: !id || !user.hasDeleteRole(UserRoles.part_category),
tooltip: t`Delete Part Category`,
onClick: () => deleteCategory.open()
})
]}
/>
@ -223,6 +273,7 @@ export default function CategoryDetail({}: {}) {
return (
<>
{editCategory.modal}
{deleteCategory.modal}
<Stack gap="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PartCategoryTree

View File

@ -1,5 +1,12 @@
import { t } from '@lingui/macro';
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
import {
Alert,
Grid,
LoadingOverlay,
Skeleton,
Stack,
Table
} from '@mantine/core';
import {
IconBookmarks,
IconBuilding,
@ -24,7 +31,7 @@ import {
} from '@tabler/icons-react';
import { useSuspenseQuery } from '@tanstack/react-query';
import { ReactNode, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { api } from '../../App';
import { DetailsField, DetailsTable } from '../../components/details/Details';
@ -32,6 +39,7 @@ import DetailsBadge from '../../components/details/DetailsBadge';
import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { PartIcons } from '../../components/details/PartIcons';
import { Thumbnail } from '../../components/images/Thumbnail';
import {
ActionDropdown,
BarcodeActionDropdown,
@ -60,6 +68,7 @@ import { InvenTreeIcon } from '../../functions/icons';
import { getDetailUrl } from '../../functions/urls';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
@ -85,6 +94,7 @@ import PartPricingPanel from './PartPricingPanel';
export default function PartDetail() {
const { id } = useParams();
const navigate = useNavigate();
const user = useUserState();
const [treeOpen, setTreeOpen] = useState(false);
@ -443,13 +453,13 @@ export default function PartDetail() {
</Grid.Col>
<Grid.Col span={8}>
<Stack gap="xs">
<table>
<tbody>
<tr>
<Table>
<Table.Tbody>
<Table.Tr>
<PartIcons part={part} />
</tr>
</tbody>
</table>
</Table.Tr>
</Table.Tbody>
</Table>
<DetailsTable fields={tl} item={part} />
</Stack>
</Grid.Col>
@ -700,6 +710,26 @@ export default function PartDetail() {
modelType: ModelType.part
});
const deletePart = useDeleteApiFormModal({
url: ApiEndpoints.part_list,
pk: part.pk,
title: t`Delete Part`,
onFormSuccess: () => {
if (part.category) {
navigate(getDetailUrl(ModelType.partcategory, part.category));
} else {
navigate('/part/');
}
},
preFormContent: (
<Alert color="red" title={t`Deleting this part cannot be reversed`}>
<Stack gap="xs">
<Thumbnail src={part.thumbnail ?? part.image} text={part.full_name} />
</Stack>
</Alert>
)
});
const stockActionProps: StockOperationProps = useMemo(() => {
return {
pk: part.pk,
@ -771,7 +801,9 @@ export default function PartDetail() {
onClick: () => editPart.open()
}),
DeleteItemAction({
hidden: part?.active || !user.hasDeleteRole(UserRoles.part)
hidden: !user.hasDeleteRole(UserRoles.part),
disabled: part.active,
onClick: () => deletePart.open()
})
]}
/>
@ -782,6 +814,7 @@ export default function PartDetail() {
<>
{duplicatePart.modal}
{editPart.modal}
{deletePart.modal}
<Stack gap="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PartCategoryTree

View File

@ -7,7 +7,7 @@ import {
IconSitemap
} from '@tabler/icons-react';
import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { ActionButton } from '../../components/buttons/ActionButton';
import { DetailsField, DetailsTable } from '../../components/details/Details';
@ -15,6 +15,7 @@ import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
ActionDropdown,
BarcodeActionDropdown,
DeleteItemAction,
EditItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
@ -34,7 +35,10 @@ import {
} from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons';
import { getDetailUrl } from '../../functions/urls';
import { useEditApiFormModal } from '../../hooks/UseForm';
import {
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import { useUserState } from '../../states/UserState';
import { PartListTable } from '../../tables/part/PartTable';
@ -49,6 +53,7 @@ export default function Stock() {
[_id]
);
const navigate = useNavigate();
const user = useUserState();
const [treeOpen, setTreeOpen] = useState(false);
@ -197,6 +202,46 @@ export default function Stock() {
onFormSuccess: refreshInstance
});
const deleteOptions = useMemo(() => {
return [
{
value: 0,
display_name: `Move items to parent location`
},
{
value: 1,
display_name: t`Delete items`
}
];
}, []);
const deleteLocation = useDeleteApiFormModal({
url: ApiEndpoints.stock_location_list,
pk: id,
title: t`Delete Stock Location`,
fields: {
delete_stock_items: {
label: t`Items Action`,
description: t`Action for stock items in this location`,
field_type: 'choice',
choices: deleteOptions
},
delete_sub_location: {
label: t`Child Locations Action`,
description: t`Action for child locations in this location`,
field_type: 'choice',
choices: deleteOptions
}
},
onFormSuccess: () => {
if (location.parent) {
navigate(getDetailUrl(ModelType.stocklocation, location.parent));
} else {
navigate('/stock/');
}
}
});
const stockItemActionProps: StockOperationProps = useMemo(() => {
return {
pk: location.pk,
@ -282,6 +327,11 @@ export default function Stock() {
hidden: !id || !user.hasChangeRole(UserRoles.stock_location),
tooltip: t`Edit Stock Location`,
onClick: () => editLocation.open()
}),
DeleteItemAction({
hidden: !id || !user.hasDeleteRole(UserRoles.stock_location),
tooltip: t`Delete Stock Location`,
onClick: () => deleteLocation.open()
})
]}
/>
@ -303,6 +353,7 @@ export default function Stock() {
return (
<>
{editLocation.modal}
{deleteLocation.modal}
<Stack>
<LoadingOverlay visible={instanceQuery.isFetching} />
<StockLocationTree

View File

@ -13,7 +13,7 @@ import {
IconSitemap
} from '@tabler/icons-react';
import { ReactNode, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details';
import DetailsBadge from '../../components/details/DetailsBadge';
@ -49,6 +49,7 @@ import { InvenTreeIcon } from '../../functions/icons';
import { getDetailUrl } from '../../functions/urls';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
@ -64,6 +65,8 @@ export default function StockDetail() {
const user = useUserState();
const navigate = useNavigate();
const [treeOpen, setTreeOpen] = useState(false);
const {
@ -370,6 +373,23 @@ export default function StockDetail() {
modelType: ModelType.stockitem
});
const preDeleteContent = useMemo(() => {
// TODO: Fill this out with information on the stock item.
// e.g. list of child items which would be deleted, etc
return undefined;
}, [stockitem]);
const deleteStockItem = useDeleteApiFormModal({
url: ApiEndpoints.stock_item_list,
pk: stockitem.pk,
title: t`Delete Stock Item`,
preFormContent: preDeleteContent,
onFormSuccess: () => {
// Redirect to the part page
navigate(getDetailUrl(ModelType.part, stockitem.part));
}
});
const stockActionProps: StockOperationProps = useMemo(() => {
return {
items: stockitem,
@ -458,7 +478,8 @@ export default function StockDetail() {
onClick: () => editStockItem.open()
}),
DeleteItemAction({
hidden: !user.hasDeleteRole(UserRoles.stock)
hidden: !user.hasDeleteRole(UserRoles.stock),
onClick: () => deleteStockItem.open()
})
]}
/>
@ -524,6 +545,7 @@ export default function StockDetail() {
<PanelGroup pageKey="stockitem" panels={stockPanels} />
{editStockItem.modal}
{duplicateStockItem.modal}
{deleteStockItem.modal}
{countStockItem.modal}
{addStockItem.modal}
{removeStockItem.modal}