[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 choices.map((choice) => {
return { return {
value: choice.value.toString(), value: choice.value.toString(),
label: choice.display_name.toString() label: choice.display_name ?? choice.value
}; };
}); });
}, [definition.choices]); }, [definition.choices]);

View File

@ -1,5 +1,5 @@
import { Trans, t } from '@lingui/macro'; 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 { FieldValues, UseControllerReturn } from 'react-hook-form';
import { InvenTreeIcon } from '../../../functions/icons'; import { InvenTreeIcon } from '../../../functions/icons';
@ -34,19 +34,21 @@ export function TableField({
return ( return (
<Table highlightOnHover striped> <Table highlightOnHover striped>
<thead> <Table.Thead>
<tr> <Table.Tr>
{definition.headers?.map((header) => { {definition.headers?.map((header) => {
return <th key={header}>{header}</th>; return <th key={header}>{header}</th>;
})} })}
</tr> </Table.Tr>
</thead> </Table.Thead>
<tbody> <Table.Tbody>
{value.length > 0 ? ( {value.length > 0 ? (
value.map((item: any, idx: number) => { value.map((item: any, idx: number) => {
// Table fields require render function // Table fields require render function
if (!definition.modelRenderer) { 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({ return definition.modelRenderer({
item: item, item: item,
@ -56,8 +58,8 @@ export function TableField({
}); });
}) })
) : ( ) : (
<tr> <Table.Tr>
<td <Table.Td
style={{ textAlign: 'center' }} style={{ textAlign: 'center' }}
colSpan={definition.headers?.length} colSpan={definition.headers?.length}
> >
@ -71,10 +73,36 @@ export function TableField({
<InvenTreeIcon icon="info" /> <InvenTreeIcon icon="info" />
<Trans>No entries available</Trans> <Trans>No entries available</Trans>
</span> </span>
</td> </Table.Td>
</tr> </Table.Tr>
)} )}
</tbody> </Table.Tbody>
</Table> </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 // Common action button for deleting an item
export function DeleteItemAction({ export function DeleteItemAction({
hidden = false, hidden = false,
disabled = false,
tooltip, tooltip,
onClick onClick
}: { }: {
hidden?: boolean; hidden?: boolean;
disabled?: boolean;
tooltip?: string; tooltip?: string;
onClick?: () => void; onClick?: () => void;
}): ActionDropdownItem { }): ActionDropdownItem {
@ -201,7 +203,8 @@ export function DeleteItemAction({
name: t`Delete`, name: t`Delete`,
tooltip: tooltip ?? t`Delete item`, tooltip: tooltip ?? t`Delete item`,
onClick: onClick, onClick: onClick,
hidden: hidden hidden: hidden,
disabled: disabled
}; };
} }

View File

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

View File

@ -1,5 +1,12 @@
import { t } from '@lingui/macro'; 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 { useDisclosure } from '@mantine/hooks';
import { import {
IconAddressBook, IconAddressBook,
@ -24,6 +31,7 @@ import {
ApiFormAdjustFilterType, ApiFormAdjustFilterType,
ApiFormFieldSet ApiFormFieldSet
} from '../components/forms/fields/ApiFormField'; } from '../components/forms/fields/ApiFormField';
import { TableFieldExtraRow } from '../components/forms/fields/TableField';
import { Thumbnail } from '../components/images/Thumbnail'; import { Thumbnail } from '../components/images/Thumbnail';
import { ProgressBar } from '../components/items/ProgressBar'; import { ProgressBar } from '../components/items/ProgressBar';
import { StylishText } from '../components/items/StylishText'; import { StylishText } from '../components/items/StylishText';
@ -308,8 +316,8 @@ function LineItemFormRow({
/> />
</FocusTrap> </FocusTrap>
</Modal> </Modal>
<tr> <Table.Tr>
<td> <Table.Td>
<Flex gap="sm" align="center"> <Flex gap="sm" align="center">
<Thumbnail <Thumbnail
size={40} size={40}
@ -318,16 +326,16 @@ function LineItemFormRow({
/> />
<div>{record.part_detail.name}</div> <div>{record.part_detail.name}</div>
</Flex> </Flex>
</td> </Table.Td>
<td>{record.supplier_part_detail.SKU}</td> <Table.Td>{record.supplier_part_detail.SKU}</Table.Td>
<td> <Table.Td>
<ProgressBar <ProgressBar
value={record.received} value={record.received}
maximum={record.quantity} maximum={record.quantity}
progressLabel progressLabel
/> />
</td> </Table.Td>
<td style={{ width: '1%', whiteSpace: 'nowrap' }}> <Table.Td style={{ width: '1%', whiteSpace: 'nowrap' }}>
<NumberInput <NumberInput
value={input.item.quantity} value={input.item.quantity}
style={{ width: '100px' }} style={{ width: '100px' }}
@ -335,8 +343,8 @@ function LineItemFormRow({
min={0} min={0}
onChange={(value) => input.changeFn(input.idx, 'quantity', value)} onChange={(value) => input.changeFn(input.idx, 'quantity', value)}
/> />
</td> </Table.Td>
<td style={{ width: '1%', whiteSpace: 'nowrap' }}> <Table.Td style={{ width: '1%', whiteSpace: 'nowrap' }}>
<Flex gap="1px"> <Flex gap="1px">
<ActionButton <ActionButton
onClick={() => locationHandlers.toggle()} onClick={() => locationHandlers.toggle()}
@ -387,11 +395,11 @@ function LineItemFormRow({
color="red" color="red"
/> />
</Flex> </Flex>
</td> </Table.Td>
</tr> </Table.Tr>
{locationOpen && ( {locationOpen && (
<tr> <Table.Tr>
<td colSpan={4}> <Table.Td colSpan={4}>
<Flex align="end" gap={5}> <Flex align="end" gap={5}>
<div style={{ flexGrow: '1' }}> <div style={{ flexGrow: '1' }}>
<StandaloneField <StandaloneField
@ -453,8 +461,8 @@ function LineItemFormRow({
)} )}
</Flex> </Flex>
</Flex> </Flex>
</td> </Table.Td>
<td> <Table.Td>
<div <div
style={{ style={{
height: '100%', height: '100%',
@ -466,107 +474,54 @@ function LineItemFormRow({
> >
<InvenTreeIcon icon="downleft" /> <InvenTreeIcon icon="downleft" />
</div> </div>
</td> </Table.Td>
</tr> </Table.Tr>
)}
{batchOpen && (
<>
<tr>
<td colSpan={4}>
<Flex align="end" gap={5}>
<div style={{ flexGrow: '1' }}>
<StandaloneField
fieldDefinition={{
field_type: 'string',
onValueChange: (value) => setBatchCode(value),
label: 'Batch Code',
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' }}>
<StandaloneField
fieldDefinition={{
field_type: 'string',
onValueChange: (value) => setSerials(value),
label: 'Serial numbers',
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}>
<StandaloneField
fieldDefinition={{
field_type: 'choice',
api_url: apiUrl(ApiEndpoints.stock_status),
choices: statuses,
label: 'Status',
onValueChange: (value) =>
input.changeFn(input.idx, 'status', value)
}}
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>
)} )}
<TableFieldExtraRow
visible={batchOpen}
colSpan={4}
content={
<StandaloneField
fieldDefinition={{
field_type: 'string',
onValueChange: (value) => setBatchCode(value),
label: 'Batch Code',
value: batchCode
}}
/>
}
/>
<TableFieldExtraRow
visible={batchOpen && record.trackable}
colSpan={4}
content={
<StandaloneField
fieldDefinition={{
field_type: 'string',
onValueChange: (value) => setSerials(value),
label: 'Serial numbers',
value: serials
}}
/>
}
/>
<TableFieldExtraRow
visible={statusOpen}
colSpan={4}
content={
<StandaloneField
fieldDefinition={{
field_type: 'choice',
api_url: apiUrl(ApiEndpoints.stock_status),
choices: statuses,
label: 'Status',
onValueChange: (value) =>
input.changeFn(input.idx, 'status', value)
}}
defaultValue={10}
/>
}
/>
</> </>
); );
} }
@ -655,11 +610,11 @@ export function useReceiveLineItems(props: LineItemsForm) {
return useCreateApiFormModal({ return useCreateApiFormModal({
...props.formProps, ...props.formProps,
url: url, url: url,
title: t`Receive line items`, title: t`Receive Line Items`,
fields: fields, fields: fields,
initialData: { initialData: {
location: null location: null
}, },
size: 'max(60%,800px)' size: 'xl'
}); });
} }

View File

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

View File

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

View File

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

View File

@ -8,12 +8,13 @@ import {
IconSitemap IconSitemap
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo, useState } from '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 { DetailsField, DetailsTable } from '../../components/details/Details';
import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { import {
ActionDropdown, ActionDropdown,
DeleteItemAction,
EditItemAction EditItemAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
@ -24,7 +25,10 @@ import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { partCategoryFields } from '../../forms/PartForms'; import { partCategoryFields } from '../../forms/PartForms';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { useEditApiFormModal } from '../../hooks/UseForm'; import {
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import ParametricPartTable from '../../tables/part/ParametricPartTable'; import ParametricPartTable from '../../tables/part/ParametricPartTable';
@ -43,6 +47,7 @@ export default function CategoryDetail({}: {}) {
[_id] [_id]
); );
const navigate = useNavigate();
const user = useUserState(); const user = useUserState();
const [treeOpen, setTreeOpen] = useState(false); const [treeOpen, setTreeOpen] = useState(false);
@ -154,6 +159,46 @@ export default function CategoryDetail({}: {}) {
onFormSuccess: refreshInstance 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(() => { const categoryActions = useMemo(() => {
return [ return [
<ActionDropdown <ActionDropdown
@ -165,6 +210,11 @@ export default function CategoryDetail({}: {}) {
hidden: !id || !user.hasChangeRole(UserRoles.part_category), hidden: !id || !user.hasChangeRole(UserRoles.part_category),
tooltip: t`Edit Part Category`, tooltip: t`Edit Part Category`,
onClick: () => editCategory.open() 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 ( return (
<> <>
{editCategory.modal} {editCategory.modal}
{deleteCategory.modal}
<Stack gap="xs"> <Stack gap="xs">
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
<PartCategoryTree <PartCategoryTree

View File

@ -1,5 +1,12 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import {
Alert,
Grid,
LoadingOverlay,
Skeleton,
Stack,
Table
} from '@mantine/core';
import { import {
IconBookmarks, IconBookmarks,
IconBuilding, IconBuilding,
@ -24,7 +31,7 @@ import {
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useSuspenseQuery } from '@tanstack/react-query'; import { useSuspenseQuery } from '@tanstack/react-query';
import { ReactNode, useMemo, useState } from 'react'; import { ReactNode, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
@ -32,6 +39,7 @@ import DetailsBadge from '../../components/details/DetailsBadge';
import { DetailsImage } from '../../components/details/DetailsImage'; import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { PartIcons } from '../../components/details/PartIcons'; import { PartIcons } from '../../components/details/PartIcons';
import { Thumbnail } from '../../components/images/Thumbnail';
import { import {
ActionDropdown, ActionDropdown,
BarcodeActionDropdown, BarcodeActionDropdown,
@ -60,6 +68,7 @@ import { InvenTreeIcon } from '../../functions/icons';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal useEditApiFormModal
} from '../../hooks/UseForm'; } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
@ -85,6 +94,7 @@ import PartPricingPanel from './PartPricingPanel';
export default function PartDetail() { export default function PartDetail() {
const { id } = useParams(); const { id } = useParams();
const navigate = useNavigate();
const user = useUserState(); const user = useUserState();
const [treeOpen, setTreeOpen] = useState(false); const [treeOpen, setTreeOpen] = useState(false);
@ -443,13 +453,13 @@ export default function PartDetail() {
</Grid.Col> </Grid.Col>
<Grid.Col span={8}> <Grid.Col span={8}>
<Stack gap="xs"> <Stack gap="xs">
<table> <Table>
<tbody> <Table.Tbody>
<tr> <Table.Tr>
<PartIcons part={part} /> <PartIcons part={part} />
</tr> </Table.Tr>
</tbody> </Table.Tbody>
</table> </Table>
<DetailsTable fields={tl} item={part} /> <DetailsTable fields={tl} item={part} />
</Stack> </Stack>
</Grid.Col> </Grid.Col>
@ -700,6 +710,26 @@ export default function PartDetail() {
modelType: ModelType.part 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(() => { const stockActionProps: StockOperationProps = useMemo(() => {
return { return {
pk: part.pk, pk: part.pk,
@ -771,7 +801,9 @@ export default function PartDetail() {
onClick: () => editPart.open() onClick: () => editPart.open()
}), }),
DeleteItemAction({ 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} {duplicatePart.modal}
{editPart.modal} {editPart.modal}
{deletePart.modal}
<Stack gap="xs"> <Stack gap="xs">
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
<PartCategoryTree <PartCategoryTree

View File

@ -7,7 +7,7 @@ import {
IconSitemap IconSitemap
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo, useState } from '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 { ActionButton } from '../../components/buttons/ActionButton';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
@ -15,6 +15,7 @@ import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { import {
ActionDropdown, ActionDropdown,
BarcodeActionDropdown, BarcodeActionDropdown,
DeleteItemAction,
EditItemAction, EditItemAction,
LinkBarcodeAction, LinkBarcodeAction,
UnlinkBarcodeAction, UnlinkBarcodeAction,
@ -34,7 +35,10 @@ import {
} from '../../forms/StockForms'; } from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeIcon } from '../../functions/icons';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { useEditApiFormModal } from '../../hooks/UseForm'; import {
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { PartListTable } from '../../tables/part/PartTable'; import { PartListTable } from '../../tables/part/PartTable';
@ -49,6 +53,7 @@ export default function Stock() {
[_id] [_id]
); );
const navigate = useNavigate();
const user = useUserState(); const user = useUserState();
const [treeOpen, setTreeOpen] = useState(false); const [treeOpen, setTreeOpen] = useState(false);
@ -197,6 +202,46 @@ export default function Stock() {
onFormSuccess: refreshInstance 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(() => { const stockItemActionProps: StockOperationProps = useMemo(() => {
return { return {
pk: location.pk, pk: location.pk,
@ -282,6 +327,11 @@ export default function Stock() {
hidden: !id || !user.hasChangeRole(UserRoles.stock_location), hidden: !id || !user.hasChangeRole(UserRoles.stock_location),
tooltip: t`Edit Stock Location`, tooltip: t`Edit Stock Location`,
onClick: () => editLocation.open() 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 ( return (
<> <>
{editLocation.modal} {editLocation.modal}
{deleteLocation.modal}
<Stack> <Stack>
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
<StockLocationTree <StockLocationTree

View File

@ -13,7 +13,7 @@ import {
IconSitemap IconSitemap
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { ReactNode, useMemo, useState } from '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 { DetailsField, DetailsTable } from '../../components/details/Details';
import DetailsBadge from '../../components/details/DetailsBadge'; import DetailsBadge from '../../components/details/DetailsBadge';
@ -49,6 +49,7 @@ import { InvenTreeIcon } from '../../functions/icons';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal useEditApiFormModal
} from '../../hooks/UseForm'; } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
@ -64,6 +65,8 @@ export default function StockDetail() {
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const [treeOpen, setTreeOpen] = useState(false); const [treeOpen, setTreeOpen] = useState(false);
const { const {
@ -370,6 +373,23 @@ export default function StockDetail() {
modelType: ModelType.stockitem 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(() => { const stockActionProps: StockOperationProps = useMemo(() => {
return { return {
items: stockitem, items: stockitem,
@ -458,7 +478,8 @@ export default function StockDetail() {
onClick: () => editStockItem.open() onClick: () => editStockItem.open()
}), }),
DeleteItemAction({ 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} /> <PanelGroup pageKey="stockitem" panels={stockPanels} />
{editStockItem.modal} {editStockItem.modal}
{duplicateStockItem.modal} {duplicateStockItem.modal}
{deleteStockItem.modal}
{countStockItem.modal} {countStockItem.modal}
{addStockItem.modal} {addStockItem.modal}
{removeStockItem.modal} {removeStockItem.modal}