diff --git a/contrib/packager.io/functions.sh b/contrib/packager.io/functions.sh index d150ed75b8..8f7f47cc66 100755 --- a/contrib/packager.io/functions.sh +++ b/contrib/packager.io/functions.sh @@ -90,7 +90,7 @@ function detect_envs() { echo "# Using existing config file: ${INVENTREE_CONFIG_FILE}" # Install parser - pip install -r ${APP_HOME}/.github/requirements.txt -q + pip install --require-hashes -r ${APP_HOME}/.github/requirements.txt -q # Load config local CONF=$(cat ${INVENTREE_CONFIG_FILE} | jc --yaml) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 5f43e0305c..0d405b6de0 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 192 +INVENTREE_API_VERSION = 193 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v193 - 2024-04-30 : https://github.com/inventree/InvenTree/pull/7144 + - Adds "assigned_to" filter to PurchaseOrder / SalesOrder / ReturnOrder API endpoints + v192 - 2024-04-23 : https://github.com/inventree/InvenTree/pull/7106 - Adds 'trackable' ordering option to BuildLineLabel API endpoint diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index 1d6692793e..9ee4df9974 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -148,6 +148,10 @@ class OrderFilter(rest_filters.FilterSet): return queryset.exclude(project_code=None) return queryset.filter(project_code=None) + assigned_to = rest_filters.ModelChoiceFilter( + queryset=Owner.objects.all(), field_name='responsible' + ) + class LineItemFilter(rest_filters.FilterSet): """Base class for custom API filters for order line item list(s).""" diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 42f6a3c69e..a1da09f21b 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -77,16 +77,18 @@ class AbstractOrderSerializer(serializers.Serializer): """Abstract serializer class which provides fields common to all order types.""" # Number of line items in this order - line_items = serializers.IntegerField(read_only=True) + line_items = serializers.IntegerField(read_only=True, label=_('Line Items')) # Number of completed line items (this is an annotated field) - completed_lines = serializers.IntegerField(read_only=True) + completed_lines = serializers.IntegerField( + read_only=True, label=_('Completed Lines') + ) # Human-readable status text (read-only) status_text = serializers.CharField(source='get_status_display', read_only=True) # status field cannot be set directly - status = serializers.IntegerField(read_only=True) + status = serializers.IntegerField(read_only=True, label=_('Order Status')) # Reference string is *required* reference = serializers.CharField(required=True) @@ -114,7 +116,9 @@ class AbstractOrderSerializer(serializers.Serializer): barcode_hash = serializers.CharField(read_only=True) - creation_date = serializers.DateField(required=False, allow_null=True) + creation_date = serializers.DateField( + required=False, allow_null=True, label=_('Creation Date') + ) def validate_reference(self, reference): """Custom validation for the reference field.""" diff --git a/src/frontend/src/components/buttons/AddItemButton.tsx b/src/frontend/src/components/buttons/AddItemButton.tsx index a9af3de8f5..adece8195f 100644 --- a/src/frontend/src/components/buttons/AddItemButton.tsx +++ b/src/frontend/src/components/buttons/AddItemButton.tsx @@ -5,6 +5,6 @@ import { ActionButton, ActionButtonProps } from './ActionButton'; /** * A generic icon button which is used to add or create a new item */ -export function AddItemButton(props: ActionButtonProps) { +export function AddItemButton(props: Readonly) { return } />; } diff --git a/src/frontend/src/components/buttons/SplitButton.tsx b/src/frontend/src/components/buttons/SplitButton.tsx index dc9700e94f..8bb73882bd 100644 --- a/src/frontend/src/components/buttons/SplitButton.tsx +++ b/src/frontend/src/components/buttons/SplitButton.tsx @@ -36,7 +36,7 @@ export function SplitButton({ selected, setSelected, loading -}: SplitButtonProps) { +}: Readonly) { const [current, setCurrent] = useState(defaultSelected); useEffect(() => { diff --git a/src/frontend/src/components/details/Details.tsx b/src/frontend/src/components/details/Details.tsx index c9b705893f..4b2260754c 100644 --- a/src/frontend/src/components/details/Details.tsx +++ b/src/frontend/src/components/details/Details.tsx @@ -180,7 +180,7 @@ function NameBadge({ pk, type }: { pk: string | number; type: BadgeType }) { * If owner is defined, only renders a badge * If user is defined, a badge is rendered in addition to main value */ -function TableStringValue(props: FieldProps) { +function TableStringValue(props: Readonly) { let value = props?.field_value; if (value === undefined) { @@ -217,11 +217,11 @@ function TableStringValue(props: FieldProps) { ); } -function BooleanValue(props: FieldProps) { +function BooleanValue(props: Readonly) { return ; } -function TableAnchorValue(props: FieldProps) { +function TableAnchorValue(props: Readonly) { if (props.field_data.external) { return ( ) { return ( ) { return ( ); diff --git a/src/frontend/src/components/details/DetailsBadge.tsx b/src/frontend/src/components/details/DetailsBadge.tsx index 9aeeef67f7..8265955896 100644 --- a/src/frontend/src/components/details/DetailsBadge.tsx +++ b/src/frontend/src/components/details/DetailsBadge.tsx @@ -7,7 +7,7 @@ export type DetailsBadgeProps = { visible?: boolean; }; -export default function DetailsBadge(props: DetailsBadgeProps) { +export default function DetailsBadge(props: Readonly) { if (props.visible == false) { return null; } diff --git a/src/frontend/src/components/details/DetailsImage.tsx b/src/frontend/src/components/details/DetailsImage.tsx index 682e2aaaa3..adebf1e4b0 100644 --- a/src/frontend/src/components/details/DetailsImage.tsx +++ b/src/frontend/src/components/details/DetailsImage.tsx @@ -322,7 +322,7 @@ function ImageActionButtons({ /** * Renders an image with action buttons for display on Details panels */ -export function DetailsImage(props: DetailImageProps) { +export function DetailsImage(props: Readonly) { // Displays a group of ActionButtons on hover const { hovered, ref } = useHover(); const [img, setImg] = useState(props.src ?? backup_image); diff --git a/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx b/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx index 26f4711429..01bc779645 100644 --- a/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx +++ b/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx @@ -87,7 +87,7 @@ type TemplateEditorProps = { template: TemplateI; }; -export function TemplateEditor(props: TemplateEditorProps) { +export function TemplateEditor(props: Readonly) { const { downloadUrl, editors, previewAreas, preview } = props; const editorRef = useRef(); const previewRef = useRef(); diff --git a/src/frontend/src/components/items/DocInfo.tsx b/src/frontend/src/components/items/DocInfo.tsx index d9012d0716..5b5d54d891 100644 --- a/src/frontend/src/components/items/DocInfo.tsx +++ b/src/frontend/src/components/items/DocInfo.tsx @@ -6,7 +6,12 @@ interface DocInfoProps extends BaseDocProps { size?: number; } -export function DocInfo({ size = 18, text, detail, link }: DocInfoProps) { +export function DocInfo({ + size = 18, + text, + detail, + link +}: Readonly) { return ( diff --git a/src/frontend/src/components/items/DocTooltip.tsx b/src/frontend/src/components/items/DocTooltip.tsx index ce2b6dc4ec..113ca234d6 100644 --- a/src/frontend/src/components/items/DocTooltip.tsx +++ b/src/frontend/src/components/items/DocTooltip.tsx @@ -21,7 +21,7 @@ export function DocTooltip({ detail, link, docchildren -}: DocTooltipProps) { +}: Readonly) { return ( ) { const progress = useMemo(() => { let maximum = props.maximum ?? 100; let value = Math.max(props.value, 0); @@ -31,8 +32,8 @@ export function ProgressBar(props: ProgressBarProps) { 100 ? 'blue' : 'green'} - size="sm" - radius="xs" + size={props.size ?? 'md'} + radius="sm" /> ); diff --git a/src/frontend/src/components/items/TitleWithDoc.tsx b/src/frontend/src/components/items/TitleWithDoc.tsx index d91c7fac97..89ca569e61 100644 --- a/src/frontend/src/components/items/TitleWithDoc.tsx +++ b/src/frontend/src/components/items/TitleWithDoc.tsx @@ -14,7 +14,7 @@ export function TitleWithDoc({ size, text, detail -}: DocTitleProps) { +}: Readonly) { return ( diff --git a/src/frontend/src/components/nav/DetailDrawer.tsx b/src/frontend/src/components/nav/DetailDrawer.tsx index add9aea45d..85a7df857b 100644 --- a/src/frontend/src/components/nav/DetailDrawer.tsx +++ b/src/frontend/src/components/nav/DetailDrawer.tsx @@ -29,7 +29,7 @@ function DetailDrawerComponent({ size, closeOnEscape = true, renderContent -}: DrawerProps) { +}: Readonly<DrawerProps>) { const navigate = useNavigate(); const { id } = useParams(); @@ -80,7 +80,7 @@ function DetailDrawerComponent({ ); } -export function DetailDrawer(props: DrawerProps) { +export function DetailDrawer(props: Readonly<DrawerProps>) { return ( <Routes> <Route path=":id?/" element={<DetailDrawerComponent {...props} />} /> diff --git a/src/frontend/src/components/nav/PageDetail.tsx b/src/frontend/src/components/nav/PageDetail.tsx index 03523e2c87..97a460376a 100644 --- a/src/frontend/src/components/nav/PageDetail.tsx +++ b/src/frontend/src/components/nav/PageDetail.tsx @@ -5,6 +5,17 @@ import { ApiImage } from '../images/ApiImage'; import { StylishText } from '../items/StylishText'; import { Breadcrumb, BreadcrumbList } from './BreadcrumbList'; +interface PageDetailInterface { + title?: string; + subtitle?: string; + imageUrl?: string; + detail?: ReactNode; + badges?: ReactNode[]; + breadcrumbs?: Breadcrumb[]; + breadcrumbAction?: () => void; + actions?: ReactNode[]; +} + /** * Construct a "standard" page detail for common display between pages. * @@ -20,16 +31,7 @@ export function PageDetail({ breadcrumbs, breadcrumbAction, actions -}: { - title?: string; - subtitle?: string; - imageUrl?: string; - detail?: ReactNode; - badges?: ReactNode[]; - breadcrumbs?: Breadcrumb[]; - breadcrumbAction?: () => void; - actions?: ReactNode[]; -}) { +}: Readonly<PageDetailInterface>) { return ( <Stack gap="xs"> {breadcrumbs && breadcrumbs.length > 0 && ( diff --git a/src/frontend/src/components/nav/PanelGroup.tsx b/src/frontend/src/components/nav/PanelGroup.tsx index 502034eb61..d88c097b54 100644 --- a/src/frontend/src/components/nav/PanelGroup.tsx +++ b/src/frontend/src/components/nav/PanelGroup.tsx @@ -50,7 +50,7 @@ function BasePanelGroup({ onPanelChange, selectedPanel, collapsible = true -}: PanelProps): ReactNode { +}: Readonly<PanelProps>): ReactNode { const navigate = useNavigate(); const { panel } = useParams(); @@ -178,7 +178,11 @@ function BasePanelGroup({ ); } -function IndexPanelComponent({ pageKey, selectedPanel, panels }: PanelProps) { +function IndexPanelComponent({ + pageKey, + selectedPanel, + panels +}: Readonly<PanelProps>) { const lastUsedPanel = useLocalState((state) => { const panelName = selectedPanel || state.lastUsedPanels[pageKey] || panels[0]?.name; @@ -203,7 +207,7 @@ function IndexPanelComponent({ pageKey, selectedPanel, panels }: PanelProps) { * @param onPanelChange - Callback when the active panel changes * @param collapsible - If true, the panel group can be collapsed (defaults to true) */ -export function PanelGroup(props: PanelProps) { +export function PanelGroup(props: Readonly<PanelProps>) { return ( <Routes> <Route index element={<IndexPanelComponent {...props} />} /> diff --git a/src/frontend/src/components/nav/SettingsHeader.tsx b/src/frontend/src/components/nav/SettingsHeader.tsx index a105187e45..a3e93423d5 100644 --- a/src/frontend/src/components/nav/SettingsHeader.tsx +++ b/src/frontend/src/components/nav/SettingsHeader.tsx @@ -3,6 +3,15 @@ import { IconSwitch } from '@tabler/icons-react'; import { ReactNode } from 'react'; import { Link } from 'react-router-dom'; +interface SettingsHeaderInterface { + title: string | ReactNode; + shorthand?: string; + subtitle?: string | ReactNode; + switch_condition?: boolean; + switch_text?: string | ReactNode; + switch_link?: string; +} + /** * Construct a settings page header with interlinks to one other settings page */ @@ -13,14 +22,7 @@ export function SettingsHeader({ switch_condition = true, switch_text, switch_link -}: { - title: string | ReactNode; - shorthand?: string; - subtitle?: string | ReactNode; - switch_condition?: boolean; - switch_text?: string | ReactNode; - switch_link?: string; -}) { +}: Readonly<SettingsHeaderInterface>) { return ( <Stack gap="0" ml={'sm'}> <Group> diff --git a/src/frontend/src/components/render/Build.tsx b/src/frontend/src/components/render/Build.tsx index fb7f3e76a3..2f40f912e4 100644 --- a/src/frontend/src/components/render/Build.tsx +++ b/src/frontend/src/components/render/Build.tsx @@ -1,13 +1,15 @@ import { ReactNode } from 'react'; import { ModelType } from '../../enums/ModelType'; -import { RenderInlineModel } from './Instance'; +import { InstanceRenderInterface, RenderInlineModel } from './Instance'; import { StatusRenderer } from './StatusRenderer'; /** * Inline rendering of a single BuildOrder instance */ -export function RenderBuildOrder({ instance }: { instance: any }): ReactNode { +export function RenderBuildOrder({ + instance +}: Readonly<InstanceRenderInterface>): ReactNode { return ( <RenderInlineModel primary={instance.reference} @@ -24,7 +26,9 @@ export function RenderBuildOrder({ instance }: { instance: any }): ReactNode { /* * Inline rendering of a single BuildLine instance */ -export function RenderBuildLine({ instance }: { instance: any }): ReactNode { +export function RenderBuildLine({ + instance +}: Readonly<InstanceRenderInterface>): ReactNode { return ( <RenderInlineModel primary={instance.part_detail.full_name} diff --git a/src/frontend/src/components/render/Company.tsx b/src/frontend/src/components/render/Company.tsx index 6a15a6a515..9dce6888d2 100644 --- a/src/frontend/src/components/render/Company.tsx +++ b/src/frontend/src/components/render/Company.tsx @@ -1,11 +1,13 @@ import { ReactNode } from 'react'; -import { RenderInlineModel } from './Instance'; +import { InstanceRenderInterface, RenderInlineModel } from './Instance'; /** * Inline rendering of a single Address instance */ -export function RenderAddress({ instance }: { instance: any }): ReactNode { +export function RenderAddress({ + instance +}: Readonly<InstanceRenderInterface>): ReactNode { let text = [ instance.country, instance.postal_code, @@ -23,7 +25,9 @@ export function RenderAddress({ instance }: { instance: any }): ReactNode { /** * Inline rendering of a single Company instance */ -export function RenderCompany({ instance }: { instance: any }): ReactNode { +export function RenderCompany({ + instance +}: Readonly<InstanceRenderInterface>): ReactNode { // TODO: Handle URL return ( @@ -38,14 +42,18 @@ export function RenderCompany({ instance }: { instance: any }): ReactNode { /** * Inline rendering of a single Contact instance */ -export function RenderContact({ instance }: { instance: any }): ReactNode { +export function RenderContact({ + instance +}: Readonly<InstanceRenderInterface>): ReactNode { return <RenderInlineModel primary={instance.name} />; } /** * Inline rendering of a single SupplierPart instance */ -export function RenderSupplierPart({ instance }: { instance: any }): ReactNode { +export function RenderSupplierPart({ + instance +}: Readonly<InstanceRenderInterface>): ReactNode { // TODO: handle URL let supplier = instance.supplier_detail ?? {}; @@ -66,9 +74,7 @@ export function RenderSupplierPart({ instance }: { instance: any }): ReactNode { */ export function RenderManufacturerPart({ instance -}: { - instance: any; -}): ReactNode { +}: Readonly<InstanceRenderInterface>): ReactNode { let part = instance.part_detail ?? {}; let manufacturer = instance.manufacturer_detail ?? {}; diff --git a/src/frontend/src/components/render/Generic.tsx b/src/frontend/src/components/render/Generic.tsx index fb616aa692..9f9a15aa33 100644 --- a/src/frontend/src/components/render/Generic.tsx +++ b/src/frontend/src/components/render/Generic.tsx @@ -1,8 +1,10 @@ import { ReactNode } from 'react'; -import { RenderInlineModel } from './Instance'; +import { InstanceRenderInterface, RenderInlineModel } from './Instance'; -export function RenderProjectCode({ instance }: { instance: any }): ReactNode { +export function RenderProjectCode({ + instance +}: Readonly<InstanceRenderInterface>): ReactNode { return ( instance && ( <RenderInlineModel diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index 086c944a47..0b70248dfb 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -37,7 +37,7 @@ type EnumDictionary<T extends string | symbol | number, U> = { */ const RendererLookup: EnumDictionary< ModelType, - (props: { instance: any }) => ReactNode + (props: Readonly<InstanceRenderInterface>) => ReactNode > = { [ModelType.address]: RenderAddress, [ModelType.build]: RenderBuildOrder, @@ -139,3 +139,7 @@ export function UnknownRenderer({ </Alert> ); } + +export interface InstanceRenderInterface { + instance: any; +} diff --git a/src/frontend/src/components/render/Order.tsx b/src/frontend/src/components/render/Order.tsx index df22461b30..63cdd581e5 100644 --- a/src/frontend/src/components/render/Order.tsx +++ b/src/frontend/src/components/render/Order.tsx @@ -2,7 +2,7 @@ import { t } from '@lingui/macro'; import { ReactNode } from 'react'; import { ModelType } from '../../enums/ModelType'; -import { RenderInlineModel } from './Instance'; +import { InstanceRenderInterface, RenderInlineModel } from './Instance'; import { StatusRenderer } from './StatusRenderer'; /** @@ -10,9 +10,7 @@ import { StatusRenderer } from './StatusRenderer'; */ export function RenderPurchaseOrder({ instance -}: { - instance: any; -}): ReactNode { +}: Readonly<InstanceRenderInterface>): ReactNode { let supplier = instance.supplier_detail || {}; // TODO: Handle URL @@ -32,7 +30,9 @@ export function RenderPurchaseOrder({ /** * Inline rendering of a single ReturnOrder instance */ -export function RenderReturnOrder({ instance }: { instance: any }): ReactNode { +export function RenderReturnOrder({ + instance +}: Readonly<InstanceRenderInterface>): ReactNode { let customer = instance.customer_detail || {}; return ( @@ -51,7 +51,9 @@ export function RenderReturnOrder({ instance }: { instance: any }): ReactNode { /** * Inline rendering of a single SalesOrder instance */ -export function RenderSalesOrder({ instance }: { instance: any }): ReactNode { +export function RenderSalesOrder({ + instance +}: Readonly<InstanceRenderInterface>): ReactNode { let customer = instance.customer_detail || {}; // TODO: Handle URL diff --git a/src/frontend/src/components/render/Part.tsx b/src/frontend/src/components/render/Part.tsx index 7a3b8a0716..c2e0ed382b 100644 --- a/src/frontend/src/components/render/Part.tsx +++ b/src/frontend/src/components/render/Part.tsx @@ -1,12 +1,14 @@ import { t } from '@lingui/macro'; import { ReactNode } from 'react'; -import { RenderInlineModel } from './Instance'; +import { InstanceRenderInterface, RenderInlineModel } from './Instance'; /** * Inline rendering of a single Part instance */ -export function RenderPart({ instance }: { instance: any }): ReactNode { +export function RenderPart({ + instance +}: Readonly<InstanceRenderInterface>): ReactNode { const stock = t`Stock` + `: ${instance.in_stock}`; return ( @@ -22,7 +24,9 @@ export function RenderPart({ instance }: { instance: any }): ReactNode { /** * Inline rendering of a PartCategory instance */ -export function RenderPartCategory({ instance }: { instance: any }): ReactNode { +export function RenderPartCategory({ + instance +}: Readonly<InstanceRenderInterface>): ReactNode { // TODO: Handle URL let lvl = '-'.repeat(instance.level || 0); diff --git a/src/frontend/src/components/render/StatusRenderer.tsx b/src/frontend/src/components/render/StatusRenderer.tsx index 78fb3f9414..5c284533dc 100644 --- a/src/frontend/src/components/render/StatusRenderer.tsx +++ b/src/frontend/src/components/render/StatusRenderer.tsx @@ -14,7 +14,7 @@ export interface StatusCodeListInterface { [key: string]: StatusCodeInterface; } -interface renderStatusLabelOptionsInterface { +interface RenderStatusLabelOptionsInterface { size?: MantineSize; } @@ -24,7 +24,7 @@ interface renderStatusLabelOptionsInterface { function renderStatusLabel( key: string | number, codes: StatusCodeListInterface, - options: renderStatusLabelOptionsInterface = {} + options: RenderStatusLabelOptionsInterface = {} ) { let text = null; let color = null; @@ -70,7 +70,7 @@ export const StatusRenderer = ({ }: { status: string | number; type: ModelType | string; - options?: renderStatusLabelOptionsInterface; + options?: RenderStatusLabelOptionsInterface; }) => { const statusCodeList = useGlobalStatusState.getState().status; diff --git a/src/frontend/src/components/render/Stock.tsx b/src/frontend/src/components/render/Stock.tsx index 7738861e98..1f8a60b995 100644 --- a/src/frontend/src/components/render/Stock.tsx +++ b/src/frontend/src/components/render/Stock.tsx @@ -1,16 +1,14 @@ import { t } from '@lingui/macro'; import { ReactNode } from 'react'; -import { RenderInlineModel } from './Instance'; +import { InstanceRenderInterface, RenderInlineModel } from './Instance'; /** * Inline rendering of a single StockLocation instance */ export function RenderStockLocation({ instance -}: { - instance: any; -}): ReactNode { +}: Readonly<InstanceRenderInterface>): ReactNode { return ( <RenderInlineModel primary={instance.name} @@ -19,7 +17,9 @@ export function RenderStockLocation({ ); } -export function RenderStockItem({ instance }: { instance: any }): ReactNode { +export function RenderStockItem({ + instance +}: Readonly<InstanceRenderInterface>): ReactNode { let quantity_string = ''; if (instance?.serial !== null && instance?.serial !== undefined) { diff --git a/src/frontend/src/components/render/User.tsx b/src/frontend/src/components/render/User.tsx index 85c508cb70..005351c0d6 100644 --- a/src/frontend/src/components/render/User.tsx +++ b/src/frontend/src/components/render/User.tsx @@ -1,9 +1,11 @@ import { IconUser, IconUsersGroup } from '@tabler/icons-react'; import { ReactNode } from 'react'; -import { RenderInlineModel } from './Instance'; +import { InstanceRenderInterface, RenderInlineModel } from './Instance'; -export function RenderOwner({ instance }: { instance: any }): ReactNode { +export function RenderOwner({ + instance +}: Readonly<InstanceRenderInterface>): ReactNode { return ( instance && ( <RenderInlineModel @@ -14,7 +16,9 @@ export function RenderOwner({ instance }: { instance: any }): ReactNode { ); } -export function RenderUser({ instance }: { instance: any }): ReactNode { +export function RenderUser({ + instance +}: Readonly<InstanceRenderInterface>): ReactNode { return ( instance && ( <RenderInlineModel diff --git a/src/frontend/src/defaults/formatters.tsx b/src/frontend/src/defaults/formatters.tsx index 395f950d45..30a52a5261 100644 --- a/src/frontend/src/defaults/formatters.tsx +++ b/src/frontend/src/defaults/formatters.tsx @@ -6,13 +6,13 @@ import { useUserSettingsState } from '../states/SettingsState'; -interface formatDecmimalOptionsType { +interface FormatDecmimalOptionsInterface { digits?: number; minDigits?: number; locale?: string; } -interface formatCurrencyOptionsType { +interface FormatCurrencyOptionsInterface { digits?: number; minDigits?: number; currency?: string; @@ -22,7 +22,7 @@ interface formatCurrencyOptionsType { export function formatDecimal( value: number | null | undefined, - options: formatDecmimalOptionsType = {} + options: FormatDecmimalOptionsInterface = {} ) { let locale = options.locale || navigator.language || 'en-US'; @@ -45,7 +45,7 @@ export function formatDecimal( */ export function formatCurrency( value: number | string | null | undefined, - options: formatCurrencyOptionsType = {} + options: FormatCurrencyOptionsInterface = {} ) { if (value == null || value == undefined) { return null; @@ -90,7 +90,7 @@ export function formatCurrency( export function formatPriceRange( minValue: number | null, maxValue: number | null, - options: formatCurrencyOptionsType = {} + options: FormatCurrencyOptionsInterface = {} ) { // If neither values are provided, return a dash if (minValue == null && maxValue == null) { @@ -117,7 +117,7 @@ export function formatPriceRange( )}`; } -interface renderDateOptionsType { +interface RenderDateOptionsInterface { showTime?: boolean; showSeconds?: boolean; } @@ -128,7 +128,10 @@ interface renderDateOptionsType { * The provided "date" variable is a string, nominally ISO format e.g. 2022-02-22 * The user-configured setting DATE_DISPLAY_FORMAT determines how the date should be displayed. */ -export function renderDate(date: string, options: renderDateOptionsType = {}) { +export function renderDate( + date: string, + options: RenderDateOptionsInterface = {} +) { if (!date) { return '-'; } diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index 82e6816abf..74f18dc7b3 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -217,7 +217,7 @@ type InvenTreeIconProps = { iconProps?: TablerIconProps; }; -export function InvenTreeIcon(props: InvenTreeIconProps) { +export function InvenTreeIcon(props: Readonly<InvenTreeIconProps>) { let Icon: React.ForwardRefExoticComponent<React.RefAttributes<any>>; if (props.icon in icons) { diff --git a/src/frontend/src/hooks/UseFilter.tsx b/src/frontend/src/hooks/UseFilter.tsx new file mode 100644 index 0000000000..9d36a6e426 --- /dev/null +++ b/src/frontend/src/hooks/UseFilter.tsx @@ -0,0 +1,91 @@ +/* + * Custom hook for retrieving a list of items from the API, + * and turning them into "filters" for use in the frontend table framework. + */ +import { useQuery } from '@tanstack/react-query'; +import { useCallback, useMemo } from 'react'; + +import { api } from '../App'; +import { ApiEndpoints } from '../enums/ApiEndpoints'; +import { resolveItem } from '../functions/conversion'; +import { apiUrl } from '../states/ApiState'; +import { TableFilterChoice } from '../tables/Filter'; + +type UseFilterProps = { + url: string; + method?: 'GET' | 'POST' | 'OPTIONS'; + params?: any; + accessor?: string; + transform: (item: any) => TableFilterChoice; +}; + +export function useFilters(props: UseFilterProps) { + const query = useQuery({ + enabled: true, + queryKey: [props.url, props.method, props.params], + queryFn: async () => { + return await api + .request({ + url: props.url, + method: props.method || 'GET', + params: props.params + }) + .then((response) => { + let data = resolveItem(response, props.accessor ?? 'data'); + + if (data == null || data == undefined) { + return []; + } + + return data; + }) + .catch((error) => []); + } + }); + + const choices: TableFilterChoice[] = useMemo(() => { + return query.data?.map(props.transform) ?? []; + }, [props.transform, query.data]); + + const refresh = useCallback(() => { + query.refetch(); + }, []); + + return { + choices, + refresh + }; +} + +// Provide list of project code filters +export function useProjectCodeFilters() { + return useFilters({ + url: apiUrl(ApiEndpoints.project_code_list), + transform: (item) => ({ + value: item.pk, + label: item.code + }) + }); +} + +// Provide list of user filters +export function useUserFilters() { + return useFilters({ + url: apiUrl(ApiEndpoints.user_list), + transform: (item) => ({ + value: item.pk, + label: item.username + }) + }); +} + +// Provide list of owner filters +export function useOwnerFilters() { + return useFilters({ + url: apiUrl(ApiEndpoints.owner_list), + transform: (item) => ({ + value: item.pk, + label: item.name + }) + }); +} diff --git a/src/frontend/src/pages/Index/Scan.tsx b/src/frontend/src/pages/Index/Scan.tsx index 563d8f1613..a8c2581e8a 100644 --- a/src/frontend/src/pages/Index/Scan.tsx +++ b/src/frontend/src/pages/Index/Scan.tsx @@ -4,6 +4,7 @@ import { Badge, Button, Checkbox, + Col, Container, Grid, Group, @@ -479,11 +480,11 @@ enum InputMethod { ImageBarcode = 'imageBarcode' } -interface inputProps { +interface ScanInputInterface { action: (items: ScanItem[]) => void; } -function InputManual({ action }: inputProps) { +function InputManual({ action }: Readonly<ScanInputInterface>) { const [value, setValue] = useState<string>(''); function btnAddItem() { @@ -535,7 +536,7 @@ function InputManual({ action }: inputProps) { } /* Input that uses QR code detection from images */ -function InputImageBarcode({ action }: inputProps) { +function InputImageBarcode({ action }: Readonly<ScanInputInterface>) { const [qrCodeScanner, setQrCodeScanner] = useState<Html5Qrcode | null>(null); const [camId, setCamId] = useLocalStorage<CameraDevice | null>({ key: 'camId', diff --git a/src/frontend/src/pages/company/CompanyDetail.tsx b/src/frontend/src/pages/company/CompanyDetail.tsx index 7a358a5911..6b00292cc0 100644 --- a/src/frontend/src/pages/company/CompanyDetail.tsx +++ b/src/frontend/src/pages/company/CompanyDetail.tsx @@ -56,7 +56,7 @@ export type CompanyDetailProps = { /** * Detail view for a single company instance */ -export default function CompanyDetail(props: CompanyDetailProps) { +export default function CompanyDetail(props: Readonly<CompanyDetailProps>) { const { id } = useParams(); const user = useUserState(); diff --git a/src/frontend/src/tables/RowActions.tsx b/src/frontend/src/tables/RowActions.tsx index cfed572f83..a30eb5bedd 100644 --- a/src/frontend/src/tables/RowActions.tsx +++ b/src/frontend/src/tables/RowActions.tsx @@ -104,7 +104,7 @@ export function RowActions({ }, [actions]); // Render a single action icon - function RowActionIcon(action: RowAction) { + function RowActionIcon(action: Readonly<RowAction>) { return ( <Tooltip withinPortal={true} diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx index 9f98e23732..4ea0f993f0 100644 --- a/src/frontend/src/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/tables/build/BuildOrderTable.tsx @@ -10,6 +10,11 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { useBuildOrderFields } from '../../forms/BuildForms'; +import { + useOwnerFilters, + useProjectCodeFilters, + useUserFilters +} from '../../hooks/UseFilter'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; @@ -92,6 +97,10 @@ export function BuildOrderTable({ }) { const tableColumns = useMemo(() => buildOrderTableColumns(), []); + const projectCodeFilters = useProjectCodeFilters(); + const userFilters = useUserFilters(); + const responsibleFilters = useOwnerFilters(); + const tableFilters: TableFilter[] = useMemo(() => { return [ { @@ -117,18 +126,36 @@ export function BuildOrderTable({ type: 'boolean', label: t`Assigned to me`, description: t`Show orders assigned to me` + }, + { + name: 'project_code', + label: t`Project Code`, + description: t`Filter by project code`, + choices: projectCodeFilters.choices + }, + { + name: 'has_project_code', + label: t`Has Project Code`, + description: t`Filter by whether the purchase order has a project code` + }, + { + name: 'issued_by', + label: t`Issued By`, + description: t`Filter by user who issued this order`, + choices: userFilters.choices + }, + { + name: 'assigned_to', + label: t`Responsible`, + description: t`Filter by responsible owner`, + choices: responsibleFilters.choices } - // TODO: 'assigned to' filter - // TODO: 'issued by' filter - // { - // name: 'has_project_code', - // title: t`Has Project Code`, - // description: t`Show orders with project code`, - // } - // TODO: 'has project code' filter (see table_filters.js) - // TODO: 'project code' filter (see table_filters.js) ]; - }, []); + }, [ + projectCodeFilters.choices, + userFilters.choices, + responsibleFilters.choices + ]); const user = useUserState(); diff --git a/src/frontend/src/tables/build/BuildOutputTable.tsx b/src/frontend/src/tables/build/BuildOutputTable.tsx index dec3deac37..31ccdd8238 100644 --- a/src/frontend/src/tables/build/BuildOutputTable.tsx +++ b/src/frontend/src/tables/build/BuildOutputTable.tsx @@ -116,13 +116,13 @@ export default function BuildOutputTable({ />, <ActionButton tooltip={t`Scrap selected outputs`} - icon={<InvenTreeIcon icon="cancel" />} + icon={<InvenTreeIcon icon="delete" />} color="red" disabled={!table.hasSelectedRecords} />, <ActionButton tooltip={t`Cancel selected outputs`} - icon={<InvenTreeIcon icon="delete" />} + icon={<InvenTreeIcon icon="cancel" />} color="red" disabled={!table.hasSelectedRecords} /> @@ -153,14 +153,14 @@ export default function BuildOutputTable({ { title: t`Scrap`, tooltip: t`Scrap build output`, - color: 'red', - icon: <InvenTreeIcon icon="cancel" /> + icon: <InvenTreeIcon icon="delete" />, + color: 'red' }, { - title: t`Delete`, - tooltip: t`Delete build output`, - color: 'red', - icon: <InvenTreeIcon icon="delete" /> + title: t`Cancel`, + tooltip: t`Cancel build output`, + icon: <InvenTreeIcon icon="cancel" />, + color: 'red' } ]; diff --git a/src/frontend/src/tables/part/PartTable.tsx b/src/frontend/src/tables/part/PartTable.tsx index 941e3aef5a..eb073e9763 100644 --- a/src/frontend/src/tables/part/PartTable.tsx +++ b/src/frontend/src/tables/part/PartTable.tsx @@ -1,7 +1,6 @@ import { t } from '@lingui/macro'; import { Group, Text } from '@mantine/core'; import { ReactNode, useMemo } from 'react'; -import { useNavigate } from 'react-router-dom'; import { AddItemButton } from '../../components/buttons/AddItemButton'; import { formatPriceRange } from '../../defaults/formatters'; @@ -9,7 +8,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { usePartFields } from '../../forms/PartForms'; -import { shortenString } from '../../functions/tables'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; @@ -43,13 +41,7 @@ function partTableColumns(): TableColumn[] { { accessor: 'category', sortable: true, - - render: function (record: any) { - // TODO: Link to the category detail page - return shortenString({ - str: record.category_detail?.pathstring - }); - } + render: (record: any) => record.category_detail?.pathstring }, { accessor: 'total_in_stock', diff --git a/src/frontend/src/tables/part/PartThumbTable.tsx b/src/frontend/src/tables/part/PartThumbTable.tsx index a29e46457a..3862ab66a0 100644 --- a/src/frontend/src/tables/part/PartThumbTable.tsx +++ b/src/frontend/src/tables/part/PartThumbTable.tsx @@ -52,7 +52,11 @@ type ThumbProps = { /** * Renders a single image thumbnail */ -function PartThumbComponent({ selected, element, selectImage }: ThumbProps) { +function PartThumbComponent({ + selected, + element, + selectImage +}: Readonly<ThumbProps>) { const { hovered, ref } = useHover(); const hoverColor = 'rgba(127,127,127,0.2)'; diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx index dd0df11d7f..845cf77f5c 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx @@ -8,6 +8,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms'; +import { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; @@ -44,6 +45,9 @@ export function PurchaseOrderTable({ const table = useTable('purchase-order'); const user = useUserState(); + const projectCodeFilters = useProjectCodeFilters(); + const responsibleFilters = useOwnerFilters(); + const tableFilters: TableFilter[] = useMemo(() => { return [ { @@ -54,11 +58,26 @@ export function PurchaseOrderTable({ }, OutstandingFilter(), OverdueFilter(), - AssignedToMeFilter() - // TODO: has_project_code - // TODO: project_code + AssignedToMeFilter(), + { + name: 'project_code', + label: t`Project Code`, + description: t`Filter by project code`, + choices: projectCodeFilters.choices + }, + { + name: 'has_project_code', + label: t`Has Project Code`, + description: t`Filter by whether the purchase order has a project code` + }, + { + name: 'assigned_to', + label: t`Responsible`, + description: t`Filter by responsible owner`, + choices: responsibleFilters.choices + } ]; - }, []); + }, [projectCodeFilters.choices, responsibleFilters.choices]); const tableColumns = useMemo(() => { return [ diff --git a/src/frontend/src/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/tables/sales/ReturnOrderTable.tsx index 73bcbfed3f..b4f77d1413 100644 --- a/src/frontend/src/tables/sales/ReturnOrderTable.tsx +++ b/src/frontend/src/tables/sales/ReturnOrderTable.tsx @@ -8,6 +8,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { useReturnOrderFields } from '../../forms/SalesOrderForms'; +import { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; @@ -35,6 +36,9 @@ export function ReturnOrderTable({ params }: { params?: any }) { const table = useTable('return-orders'); const user = useUserState(); + const projectCodeFilters = useProjectCodeFilters(); + const responsibleFilters = useOwnerFilters(); + const tableFilters: TableFilter[] = useMemo(() => { return [ { @@ -45,9 +49,26 @@ export function ReturnOrderTable({ params }: { params?: any }) { }, OutstandingFilter(), OverdueFilter(), - AssignedToMeFilter() + AssignedToMeFilter(), + { + name: 'project_code', + label: t`Project Code`, + description: t`Filter by project code`, + choices: projectCodeFilters.choices + }, + { + name: 'has_project_code', + label: t`Has Project Code`, + description: t`Filter by whether the purchase order has a project code` + }, + { + name: 'assigned_to', + label: t`Responsible`, + description: t`Filter by responsible owner`, + choices: responsibleFilters.choices + } ]; - }, []); + }, [projectCodeFilters.choices, responsibleFilters.choices]); const tableColumns = useMemo(() => { return [ diff --git a/src/frontend/src/tables/sales/SalesOrderTable.tsx b/src/frontend/src/tables/sales/SalesOrderTable.tsx index c3556e6278..d48f72a4b9 100644 --- a/src/frontend/src/tables/sales/SalesOrderTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderTable.tsx @@ -8,6 +8,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { useSalesOrderFields } from '../../forms/SalesOrderForms'; +import { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; @@ -41,6 +42,9 @@ export function SalesOrderTable({ const table = useTable('sales-order'); const user = useUserState(); + const projectCodeFilters = useProjectCodeFilters(); + const responsibleFilters = useOwnerFilters(); + const tableFilters: TableFilter[] = useMemo(() => { return [ { @@ -51,11 +55,26 @@ export function SalesOrderTable({ }, OutstandingFilter(), OverdueFilter(), - AssignedToMeFilter() - // TODO: has_project_code - // TODO: project_code + AssignedToMeFilter(), + { + name: 'project_code', + label: t`Project Code`, + description: t`Filter by project code`, + choices: projectCodeFilters.choices + }, + { + name: 'has_project_code', + label: t`Has Project Code`, + description: t`Filter by whether the purchase order has a project code` + }, + { + name: 'assigned_to', + label: t`Responsible`, + description: t`Filter by responsible owner`, + choices: responsibleFilters.choices + } ]; - }, []); + }, [projectCodeFilters.choices, responsibleFilters.choices]); const salesOrderFields = useSalesOrderFields(); diff --git a/src/frontend/src/tables/stock/StockItemTable.tsx b/src/frontend/src/tables/stock/StockItemTable.tsx index 95a04e2f83..8ea7ac4ce3 100644 --- a/src/frontend/src/tables/stock/StockItemTable.tsx +++ b/src/frontend/src/tables/stock/StockItemTable.tsx @@ -4,7 +4,7 @@ import { ReactNode, useMemo } from 'react'; import { AddItemButton } from '../../components/buttons/AddItemButton'; import { ActionDropdown } from '../../components/items/ActionDropdown'; -import { formatCurrency, renderDate } from '../../defaults/formatters'; +import { formatCurrency } from '../../defaults/formatters'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; diff --git a/src/frontend/tests/pui_general.spec.ts b/src/frontend/tests/pui_general.spec.ts index 91a0d1f7d6..8f860d72fe 100644 --- a/src/frontend/tests/pui_general.spec.ts +++ b/src/frontend/tests/pui_general.spec.ts @@ -27,10 +27,19 @@ test('PUI - Parts', async ({ page }) => { await page.getByText('1551ACLR').click(); await page.getByRole('tab', { name: 'Part Details' }).click(); await page.getByRole('tab', { name: 'Parameters' }).click(); - // await page.getByRole('tab', { name: 'Stock' }).click(); + await page + .getByRole('tab', { name: 'Part Details' }) + .locator('xpath=..') + .getByRole('tab', { name: 'Stock', exact: true }) + .click(); await page.getByRole('tab', { name: 'Allocations' }).click(); await page.getByRole('tab', { name: 'Used In' }).click(); await page.getByRole('tab', { name: 'Pricing' }).click(); + + await page.goto(`${baseUrl}/part/category/index/parts`); + await page.getByText('Blue Chair').click(); + await page.getByRole('tab', { name: 'Bill of Materials' }).click(); + await page.getByRole('tab', { name: 'Build Orders' }).click(); }); test('PUI - Parts - Manufacturer Parts', async ({ page }) => { diff --git a/src/frontend/tests/pui_stock.spec.ts b/src/frontend/tests/pui_stock.spec.ts index 97c8aa8ba6..f146c4ebd6 100644 --- a/src/frontend/tests/pui_stock.spec.ts +++ b/src/frontend/tests/pui_stock.spec.ts @@ -16,6 +16,14 @@ test('PUI - Stock', async ({ page }) => { await page.getByRole('tab', { name: 'Stock Locations' }).click(); await page.getByRole('tab', { name: 'Stock Items' }).click(); await page.getByRole('tab', { name: 'Location Details' }).click(); + + await page.goto(`${baseUrl}/stock/item/1194/details`); + await page.getByText('D.123 | Doohickey').waitFor(); + await page.getByText('Batch Code: BX-123-2024-2-7').waitFor(); + await page.getByRole('tab', { name: 'Stock Tracking' }).click(); + await page.getByRole('tab', { name: 'Test Data' }).click(); + await page.getByText('395c6d5586e5fb656901d047be27e1f7').waitFor(); + await page.getByRole('tab', { name: 'Installed Items' }).click(); }); test('PUI - Build', async ({ page }) => {