diff --git a/src/frontend/src/components/forms/fields/ChoiceField.tsx b/src/frontend/src/components/forms/fields/ChoiceField.tsx index a65f630463..5b7757645d 100644 --- a/src/frontend/src/components/forms/fields/ChoiceField.tsx +++ b/src/frontend/src/components/forms/fields/ChoiceField.tsx @@ -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]); diff --git a/src/frontend/src/components/forms/fields/TableField.tsx b/src/frontend/src/components/forms/fields/TableField.tsx index baf4aa5968..302c2f0210 100644 --- a/src/frontend/src/components/forms/fields/TableField.tsx +++ b/src/frontend/src/components/forms/fields/TableField.tsx @@ -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 ( - - + + {definition.headers?.map((header) => { return ; })} - - - + + + {value.length > 0 ? ( value.map((item: any, idx: number) => { // Table fields require render function if (!definition.modelRenderer) { - return {t`modelRenderer entry required for tables`}; + return ( + {t`modelRenderer entry required for tables`} + ); } return definition.modelRenderer({ item: item, @@ -56,8 +58,8 @@ export function TableField({ }); }) ) : ( - - - + + )} - +
{header}
+ @@ -71,10 +73,36 @@ export function TableField({ No entries available -
); } + +/* + * 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 && ( + + + + + {content} + + + + ) + ); +} diff --git a/src/frontend/src/components/items/ActionDropdown.tsx b/src/frontend/src/components/items/ActionDropdown.tsx index dd91918737..59045bb928 100644 --- a/src/frontend/src/components/items/ActionDropdown.tsx +++ b/src/frontend/src/components/items/ActionDropdown.tsx @@ -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 }; } diff --git a/src/frontend/src/components/modals/ServerInfoModal.tsx b/src/frontend/src/components/modals/ServerInfoModal.tsx index f74e4a6ec5..7fad8a6fda 100644 --- a/src/frontend/src/components/modals/ServerInfoModal.tsx +++ b/src/frontend/src/components/modals/ServerInfoModal.tsx @@ -26,46 +26,46 @@ export function ServerInfoModal({ Server - - - - - - - - - + + {server.debug_mode && ( - - - - + + )} {server.docker_mode && ( - - - - + + )} - - - - - - - - + + {server.worker_running != true && ( - - - - + + )} {server.email_configured != true && ( - - - - + + )} - +
+ + + Instance Name - {server.instance}
+ + {server.instance} + + + Database - + + {server.database} -
+ + Debug Mode - + + Server is running in debug mode -
+ + Docker Mode - + + Server is deployed using docker -
+ + Plugin Support - + + {server.plugins_enabled ? ( Plugin support enabled @@ -73,13 +73,13 @@ export function ServerInfoModal({ Plugin support disabled )} -
+ + + + Server status - + + {server.system_health ? ( @@ -89,52 +89,52 @@ export function ServerInfoModal({ )} -
+ + Background Worker - + + Background worker not running -
+ + Email Settings - + + Email settings not configured -
<Trans>Version</Trans> - - - - - - - - - - + + {server.apiVersion} + +
+ + + Server Version - {server.version}
+ + {server.version} + + + API Version - {server.apiVersion}
diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index 930e729571..a5ad45d826 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -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({ /> - - + +
{record.part_detail.name}
- - {record.supplier_part_detail.SKU} - +
+ {record.supplier_part_detail.SKU} + - - + + input.changeFn(input.idx, 'quantity', value)} /> - - + + locationHandlers.toggle()} @@ -387,11 +395,11 @@ function LineItemFormRow({ color="red" /> - - + +
{locationOpen && ( - - + +
- - + +
- - - )} - {batchOpen && ( - <> - - - -
- setBatchCode(value), - label: 'Batch Code', - value: batchCode - }} - /> -
-
- - -
- - -
- - - {record.trackable && ( - - - -
- setSerials(value), - label: 'Serial numbers', - value: serials - }} - /> -
-
- - -
- - -
- - - )} - - )} - {statusOpen && ( - - - - input.changeFn(input.idx, 'status', value) - }} - defaultValue={10} - /> - - -
- - - -
- - +
+ )} + setBatchCode(value), + label: 'Batch Code', + value: batchCode + }} + /> + } + /> + setSerials(value), + label: 'Serial numbers', + value: serials + }} + /> + } + /> + + input.changeFn(input.idx, 'status', value) + }} + defaultValue={10} + /> + } + /> ); } @@ -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' }); } diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index d7dfedc0e8..eb8e0bad27 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -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 ? (
{t`Loading...`}
) : ( - - + +
{record.part_detail?.name}
- - {record.location ? record.location_detail?.pathstring : '-'} - +
+ + {record.location ? record.location_detail?.pathstring : '-'} + + {stockString} - + {!merge && ( - + - + )} - + {transfer && ( - - + +
); } diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index 6484003eb1..94ab038dd0 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -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, diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/UserThemePanel.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/UserThemePanel.tsx index 7765e888a3..fa3bb9cf70 100644 --- a/src/frontend/src/pages/Index/Settings/AccountSettings/UserThemePanel.tsx +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/UserThemePanel.tsx @@ -81,41 +81,41 @@ export function UserTheme({ height }: { height: number }) { Theme - - - - - - - - - - - - - - - - - - - - - - + + +
+ + + Primary color - + + -
+ + + + White color - + + -
+ + + + Black color - + + -
+ + + + Border Radius - + + getMark(val).label} defaultValue={50} @@ -125,13 +125,13 @@ export function UserTheme({ height }: { height: number }) { onChange={changeRadius} mb={18} /> -
+ + + + Loader - + +
); diff --git a/src/frontend/src/pages/part/CategoryDetail.tsx b/src/frontend/src/pages/part/CategoryDetail.tsx index 9db7350807..d82ad4c29a 100644 --- a/src/frontend/src/pages/part/CategoryDetail.tsx +++ b/src/frontend/src/pages/part/CategoryDetail.tsx @@ -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 [ 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} - - - +
+ + - - -
+ + +
@@ -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: ( + + + + + + ) + }); + 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} { + 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} { + // 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() { {editStockItem.modal} {duplicateStockItem.modal} + {deleteStockItem.modal} {countStockItem.modal} {addStockItem.modal} {removeStockItem.modal}