diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx
index 315121efef..6b49748fed 100644
--- a/src/frontend/src/components/forms/ApiForm.tsx
+++ b/src/frontend/src/components/forms/ApiForm.tsx
@@ -19,9 +19,11 @@ import {
SubmitHandler,
useForm
} from 'react-hook-form';
+import { useNavigate } from 'react-router-dom';
import { api, queryClient } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
+import { ModelType } from '../../enums/ModelType';
import {
NestedDict,
constructField,
@@ -30,6 +32,7 @@ import {
mapFields
} from '../../functions/forms';
import { invalidResponse } from '../../functions/notifications';
+import { getDetailUrl } from '../../functions/urls';
import { PathParams } from '../../states/ApiState';
import {
ApiFormField,
@@ -59,6 +62,8 @@ export interface ApiFormAction {
* @param successMessage : Optional message to display on successful form submission
* @param onFormSuccess : A callback function to call when the form is submitted successfully.
* @param onFormError : A callback function to call when the form is submitted with errors.
+ * @param modelType : Define a model type for this form
+ * @param follow : Boolean, follow the result of the form (if possible)
*/
export interface ApiFormProps {
url: ApiEndpoints | string;
@@ -79,6 +84,8 @@ export interface ApiFormProps {
successMessage?: string;
onFormSuccess?: (data: any) => void;
onFormError?: () => void;
+ modelType?: ModelType;
+ follow?: boolean;
actions?: ApiFormAction[];
timeout?: number;
}
@@ -183,6 +190,8 @@ export function ApiForm({
props: ApiFormProps;
optionsLoading: boolean;
}) {
+ const navigate = useNavigate();
+
const fields: ApiFormFieldSet = useMemo(() => {
return props.fields ?? {};
}, [props.fields]);
@@ -384,6 +393,12 @@ export function ApiForm({
props.onFormSuccess(response.data);
}
+ if (props.follow) {
+ if (props.modelType && response.data?.pk) {
+ navigate(getDetailUrl(props.modelType, response.data?.pk));
+ }
+ }
+
// Optionally show a success message
if (props.successMessage) {
notifications.hide('form-success');
diff --git a/src/frontend/src/components/items/ActionDropdown.tsx b/src/frontend/src/components/items/ActionDropdown.tsx
index e3c2f951f1..a677bd6ee5 100644
--- a/src/frontend/src/components/items/ActionDropdown.tsx
+++ b/src/frontend/src/components/items/ActionDropdown.tsx
@@ -14,8 +14,10 @@ import {
IconTrash,
IconUnlink
} from '@tabler/icons-react';
+import { color } from '@uiw/react-codemirror';
import { ReactNode, useMemo } from 'react';
+import { InvenTreeIcon } from '../../functions/icons';
import { notYetImplemented } from '../../functions/notifications';
export type ActionDropdownItem = {
@@ -203,6 +205,24 @@ export function DeleteItemAction({
};
}
+export function CancelItemAction({
+ hidden = false,
+ tooltip,
+ onClick
+}: {
+ hidden?: boolean;
+ tooltip?: string;
+ onClick?: () => void;
+}): ActionDropdownItem {
+ return {
+ icon: ,
+ name: t`Cancel`,
+ tooltip: tooltip ?? t`Cancel`,
+ onClick: onClick,
+ hidden: hidden
+ };
+}
+
// Common action button for duplicating an item
export function DuplicateItemAction({
hidden = false,
diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx
index fc023688af..ccc7276ab5 100644
--- a/src/frontend/src/forms/StockForms.tsx
+++ b/src/frontend/src/forms/StockForms.tsx
@@ -268,8 +268,6 @@ function StockOperationsRow({
}) {
const item = input.item;
- console.log('rec', record);
-
const [value, setValue] = useState(
add ? 0 : item.quantity ?? 0
);
diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx
index b362d08ce5..486cb43343 100644
--- a/src/frontend/src/functions/icons.tsx
+++ b/src/frontend/src/functions/icons.tsx
@@ -12,8 +12,10 @@ import {
IconCalendarStats,
IconCategory,
IconCheck,
+ IconCircleCheck,
IconCircleMinus,
IconCirclePlus,
+ IconCircleX,
IconClipboardList,
IconClipboardText,
IconCopy,
@@ -59,6 +61,7 @@ import {
IconTool,
IconTools,
IconTransfer,
+ IconTransitionRight,
IconTrash,
IconTruck,
IconTruckDelivery,
@@ -130,6 +133,10 @@ const icons = {
delete: IconTrash,
packaging: IconPackage,
packages: IconPackages,
+ install: IconTransitionRight,
+ plus: IconCirclePlus,
+ minus: IconCircleMinus,
+ cancel: IconCircleX,
// Part Icons
active: IconCheck,
@@ -186,7 +193,8 @@ const icons = {
batch_code: IconClipboardText,
destination: IconFlag,
repeat_destination: IconFlagShare,
- unlink: IconUnlink
+ unlink: IconUnlink,
+ success: IconCircleCheck
};
export type InvenTreeIconType = keyof typeof icons;
diff --git a/src/frontend/src/hooks/UseTable.tsx b/src/frontend/src/hooks/UseTable.tsx
index 010085861b..19ed88aa56 100644
--- a/src/frontend/src/hooks/UseTable.tsx
+++ b/src/frontend/src/hooks/UseTable.tsx
@@ -1,5 +1,5 @@
import { randomId, useLocalStorage } from '@mantine/hooks';
-import { useCallback, useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
import { TableFilter } from '../tables/Filter';
@@ -22,6 +22,7 @@ export type TableState = {
expandedRecords: any[];
setExpandedRecords: (records: any[]) => void;
selectedRecords: any[];
+ hasSelectedRecords: boolean;
setSelectedRecords: (records: any[]) => void;
clearSelectedRecords: () => void;
hiddenColumns: string[];
@@ -78,6 +79,11 @@ export function useTable(tableName: string): TableState {
setSelectedRecords([]);
}, []);
+ const hasSelectedRecords = useMemo(
+ () => selectedRecords.length > 0,
+ [selectedRecords]
+ );
+
// Total record count
const [recordCount, setRecordCount] = useState(0);
@@ -126,6 +132,7 @@ export function useTable(tableName: string): TableState {
selectedRecords,
setSelectedRecords,
clearSelectedRecords,
+ hasSelectedRecords,
hiddenColumns,
setHiddenColumns,
searchTerm,
diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx
index 1f6bb03f0d..0644ad90aa 100644
--- a/src/frontend/src/pages/build/BuildDetail.tsx
+++ b/src/frontend/src/pages/build/BuildDetail.tsx
@@ -22,6 +22,7 @@ import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
ActionDropdown,
+ CancelItemAction,
DuplicateItemAction,
EditItemAction,
LinkBarcodeAction,
@@ -36,12 +37,16 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useBuildOrderFields } from '../../forms/BuildForms';
-import { useEditApiFormModal } from '../../hooks/UseForm';
+import {
+ useCreateApiFormModal,
+ useEditApiFormModal
+} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import BuildLineTable from '../../tables/build/BuildLineTable';
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
+import BuildOutputTable from '../../tables/build/BuildOutputTable';
import { AttachmentTable } from '../../tables/general/AttachmentTable';
import { StockItemTable } from '../../tables/stock/StockItemTable';
@@ -213,7 +218,12 @@ export default function BuildDetail() {
{
name: 'incomplete-outputs',
label: t`Incomplete Outputs`,
- icon:
+ icon: ,
+ content: build.pk ? (
+
+ ) : (
+
+ )
// TODO: Hide if build is complete
},
{
@@ -290,6 +300,18 @@ export default function BuildDetail() {
}
});
+ const duplicateBuild = useCreateApiFormModal({
+ url: ApiEndpoints.build_order_list,
+ title: t`Add Build Order`,
+ fields: buildOrderFields,
+ initialData: {
+ ...build,
+ reference: undefined
+ },
+ follow: true,
+ modelType: ModelType.build
+ });
+
const buildActions = useMemo(() => {
// TODO: Disable certain actions based on user permissions
return [
@@ -328,7 +350,13 @@ export default function BuildDetail() {
onClick: () => editBuild.open(),
hidden: !user.hasChangeRole(UserRoles.build)
}),
- DuplicateItemAction({})
+ CancelItemAction({
+ tooltip: t`Cancel order`
+ }),
+ DuplicateItemAction({
+ onClick: () => duplicateBuild.open(),
+ hidden: !user.hasAddRole(UserRoles.build)
+ })
]}
/>
];
@@ -349,6 +377,7 @@ export default function BuildDetail() {
return (
<>
{editBuild.modal}
+ {duplicateBuild.modal}
{
return [
}
actions={[
DuplicateItemAction({
- hidden: !user.hasAddRole(UserRoles.purchase_order)
+ hidden: !user.hasAddRole(UserRoles.purchase_order),
+ onClick: () => duplicateManufacturerPart.open()
}),
EditItemAction({
hidden: !user.hasChangeRole(UserRoles.purchase_order),
@@ -227,6 +242,7 @@ export default function ManufacturerPartDetail() {
return (
<>
{editManufacturerPart.modal}
+ {duplicateManufacturerPart.modal}
{
- if (response.pk) {
- navigate(getDetailUrl(ModelType.supplierpart, response.pk));
- }
- }
+ follow: true,
+ modelType: ModelType.supplierpart
});
const breadcrumbs = useMemo(() => {
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index 09a1dfa36b..859048e5e5 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -24,7 +24,7 @@ import {
} from '@tabler/icons-react';
import { useSuspenseQuery } from '@tanstack/react-query';
import { ReactNode, useMemo, useState } from 'react';
-import { useNavigate, useParams } from 'react-router-dom';
+import { useParams } from 'react-router-dom';
import { api } from '../../App';
import { DetailsField, DetailsTable } from '../../components/details/Details';
@@ -86,7 +86,6 @@ export default function PartDetail() {
const { id } = useParams();
const user = useUserState();
- const navigate = useNavigate();
const [treeOpen, setTreeOpen] = useState(false);
@@ -680,11 +679,8 @@ export default function PartDetail() {
initialData: {
...part
},
- onFormSuccess: (response: any) => {
- if (response.pk) {
- navigate(getDetailUrl(ModelType.part, response.pk));
- }
- }
+ follow: true,
+ modelType: ModelType.part
});
const stockActionProps: StockOperationProps = useMemo(() => {
diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
index a982cf3b23..b0485ed1ec 100644
--- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
+++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
@@ -9,7 +9,7 @@ import {
IconPaperclip
} from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react';
-import { useParams } from 'react-router-dom';
+import { useNavigate, useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details';
import { DetailsImage } from '../../components/details/DetailsImage';
@@ -17,7 +17,8 @@ import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
ActionDropdown,
BarcodeActionDropdown,
- DeleteItemAction,
+ CancelItemAction,
+ DuplicateItemAction,
EditItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
@@ -31,7 +32,11 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
-import { useEditApiFormModal } from '../../hooks/UseForm';
+import { getDetailUrl } from '../../functions/urls';
+import {
+ useCreateApiFormModal,
+ useEditApiFormModal
+} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
@@ -46,6 +51,7 @@ export default function PurchaseOrderDetail() {
const { id } = useParams();
const user = useUserState();
+ const navigate = useNavigate();
const {
instance: order,
@@ -72,6 +78,18 @@ export default function PurchaseOrderDetail() {
}
});
+ const duplicatePurchaseOrder = useCreateApiFormModal({
+ url: ApiEndpoints.purchase_order_list,
+ title: t`Add Purchase Order`,
+ fields: purchaseOrderFields,
+ initialData: {
+ ...order,
+ reference: undefined
+ },
+ follow: true,
+ modelType: ModelType.purchaseorder
+ });
+
const detailsPanel = useMemo(() => {
if (instanceQuery.isFetching) {
return ;
@@ -299,8 +317,12 @@ export default function PurchaseOrderDetail() {
editPurchaseOrder.open();
}
}),
- DeleteItemAction({
- hidden: !user.hasDeleteRole(UserRoles.purchase_order)
+ CancelItemAction({
+ tooltip: t`Cancel order`
+ }),
+ DuplicateItemAction({
+ hidden: !user.hasAddRole(UserRoles.purchase_order),
+ onClick: () => duplicatePurchaseOrder.open()
})
]}
/>
@@ -322,6 +344,7 @@ export default function PurchaseOrderDetail() {
return (
<>
{editPurchaseOrder.modal}
+ {duplicatePurchaseOrder.modal}
{
return [
duplicateReturnOrder.open()
})
]}
/>
@@ -285,6 +306,7 @@ export default function ReturnOrderDetail() {
return (
<>
{editReturnOrder.modal}
+ {duplicateReturnOrder.modal}
{
return [
{
@@ -281,13 +300,14 @@ export default function SalesOrderDetail() {
actions={[
EditItemAction({
hidden: !user.hasChangeRole(UserRoles.sales_order),
- onClick: () => {
- editSalesOrder.open();
- }
+ onClick: () => editSalesOrder.open()
}),
- DeleteItemAction({
- hidden: !user.hasDeleteRole(UserRoles.sales_order)
- // TODO: Delete?
+ CancelItemAction({
+ tooltip: t`Cancel order`
+ }),
+ DuplicateItemAction({
+ hidden: !user.hasAddRole(UserRoles.sales_order),
+ onClick: () => duplicateSalesOrder.open()
})
]}
/>
@@ -309,6 +329,7 @@ export default function SalesOrderDetail() {
return (
<>
{editSalesOrder.modal}
+ {duplicateSalesOrder.modal}
{
- if (response.pk) {
- navigate(getDetailUrl(ModelType.stockitem, response.pk));
- }
- }
+ follow: true,
+ modelType: ModelType.stockitem
});
const stockActionProps: StockOperationProps = useMemo(() => {
@@ -479,6 +464,11 @@ export default function StockDetail() {
return instanceQuery.isLoading
? []
: [
+ ,
{
+ let location = resolveItem(record, accessor);
+
+ if (!location) {
+ return {t`No location set`};
+ }
+
+ return {location.name};
+ }
+ };
+}
+
export function BooleanColumn({
accessor,
title,
diff --git a/src/frontend/src/tables/TableHoverCard.tsx b/src/frontend/src/tables/TableHoverCard.tsx
index 8486845638..93893ab834 100644
--- a/src/frontend/src/tables/TableHoverCard.tsx
+++ b/src/frontend/src/tables/TableHoverCard.tsx
@@ -27,7 +27,7 @@ export function TableHoverCard({
}
return (
-
+
{value}
diff --git a/src/frontend/src/tables/bom/BomTable.tsx b/src/frontend/src/tables/bom/BomTable.tsx
index 06e7603347..41b358318a 100644
--- a/src/frontend/src/tables/bom/BomTable.tsx
+++ b/src/frontend/src/tables/bom/BomTable.tsx
@@ -298,7 +298,7 @@ export function BomTable({
const newBomItem = useCreateApiFormModal({
url: ApiEndpoints.bom_list,
- title: t`Create BOM Item`,
+ title: t`Add BOM Item`,
fields: bomItemFields(),
initialData: {
part: partId
diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx
index 19f9438380..cfc0a61346 100644
--- a/src/frontend/src/tables/build/BuildOrderTable.tsx
+++ b/src/frontend/src/tables/build/BuildOrderTable.tsx
@@ -1,6 +1,5 @@
import { t } from '@lingui/macro';
import { useMemo } from 'react';
-import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { PartHoverCard } from '../../components/images/Thumbnail';
@@ -11,7 +10,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useBuildOrderFields } from '../../forms/BuildForms';
-import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
@@ -130,7 +128,6 @@ export function BuildOrderTable({
];
}, []);
- const navigate = useNavigate();
const user = useUserState();
const table = useTable('buildorder');
@@ -146,11 +143,8 @@ export function BuildOrderTable({
sales_order: salesOrderId,
parent: parentBuildId
},
- onFormSuccess: (data: any) => {
- if (data.pk) {
- navigate(getDetailUrl(ModelType.build, data.pk));
- }
- }
+ follow: true,
+ modelType: ModelType.build
});
const tableActions = useMemo(() => {
diff --git a/src/frontend/src/tables/build/BuildOutputTable.tsx b/src/frontend/src/tables/build/BuildOutputTable.tsx
new file mode 100644
index 0000000000..657151909d
--- /dev/null
+++ b/src/frontend/src/tables/build/BuildOutputTable.tsx
@@ -0,0 +1,284 @@
+import { t } from '@lingui/macro';
+import { Group, Text } from '@mantine/core';
+import {
+ IconCircleCheck,
+ IconCircleX,
+ IconExclamationCircle
+} from '@tabler/icons-react';
+import { useQuery } from '@tanstack/react-query';
+import { useCallback, useMemo } from 'react';
+
+import { api } from '../../App';
+import { ActionButton } from '../../components/buttons/ActionButton';
+import { AddItemButton } from '../../components/buttons/AddItemButton';
+import { ProgressBar } from '../../components/items/ProgressBar';
+import { ApiEndpoints } from '../../enums/ApiEndpoints';
+import { ModelType } from '../../enums/ModelType';
+import { UserRoles } from '../../enums/Roles';
+import { InvenTreeIcon } from '../../functions/icons';
+import { useTable } from '../../hooks/UseTable';
+import { apiUrl } from '../../states/ApiState';
+import { useUserState } from '../../states/UserState';
+import { TableColumn } from '../Column';
+import { LocationColumn, PartColumn } from '../ColumnRenderers';
+import { InvenTreeTable } from '../InvenTreeTable';
+import { RowAction } from '../RowActions';
+import { TableHoverCard } from '../TableHoverCard';
+
+type TestResultOverview = {
+ name: string;
+ result: boolean;
+};
+
+export default function BuildOutputTable({
+ buildId,
+ partId
+}: {
+ buildId: number;
+ partId: number;
+}) {
+ const user = useUserState();
+ const table = useTable('build-outputs');
+
+ // Fetch the test templates associated with the partId
+ const { data: testTemplates } = useQuery({
+ queryKey: ['buildoutputtests', partId],
+ queryFn: async () => {
+ if (!partId) {
+ return [];
+ }
+
+ return api
+ .get(apiUrl(ApiEndpoints.part_test_template_list), {
+ params: {
+ part: partId,
+ include_inherited: true,
+ enabled: true,
+ required: true
+ }
+ })
+ .then((response) => response.data)
+ .catch(() => []);
+ }
+ });
+
+ const hasRequiredTests: boolean = useMemo(() => {
+ return (testTemplates?.length ?? 0) > 0;
+ }, [partId, testTemplates]);
+
+ // Format table records
+ const formatRecords = useCallback(
+ (records: any[]): any[] => {
+ records?.forEach((record: any, index: number) => {
+ let results: TestResultOverview[] = [];
+ let passCount: number = 0;
+
+ // Iterate through each
+ testTemplates?.forEach((template: any) => {
+ // Find the "newest" result for this template in the returned data
+ let result = record.tests
+ ?.filter((test: any) => test.template == template.pk)
+ .sort((a: any, b: any) => {
+ return a.pk < b.pk ? 1 : -1;
+ })
+ .shift();
+
+ if (template?.required && result?.result) {
+ passCount += 1;
+ }
+
+ results.push({
+ name: template.test_name,
+ result: result?.result ?? false
+ });
+ });
+
+ records[index].passCount = passCount;
+ records[index].results = results;
+ });
+
+ return records;
+ },
+ [partId, testTemplates]
+ );
+
+ const tableActions = useMemo(() => {
+ // TODO: Button to create new build output
+ // TODO: Button to complete output(s)
+ // TODO: Button to cancel output(s)
+ // TODO: Button to scrap output(s)
+ return [
+ ,
+ }
+ color="green"
+ disabled={!table.hasSelectedRecords}
+ />,
+ }
+ color="red"
+ disabled={!table.hasSelectedRecords}
+ />,
+ }
+ color="red"
+ disabled={!table.hasSelectedRecords}
+ />
+ ];
+ }, [user, partId, buildId, table.hasSelectedRecords]);
+
+ const rowActions = useCallback(
+ (record: any) => {
+ let actions: RowAction[] = [
+ {
+ title: t`Allocate`,
+ tooltip: t`Allocate stock to build output`,
+ color: 'blue',
+ icon:
+ },
+ {
+ title: t`Deallocate`,
+ tooltip: t`Deallocate stock from build output`,
+ color: 'red',
+ icon:
+ },
+ {
+ title: t`Complete`,
+ tooltip: t`Complete build output`,
+ color: 'green',
+ icon:
+ },
+ {
+ title: t`Scrap`,
+ tooltip: t`Scrap build output`,
+ color: 'red',
+ icon:
+ },
+ {
+ title: t`Delete`,
+ tooltip: t`Delete build output`,
+ color: 'red',
+ icon:
+ }
+ ];
+
+ return actions;
+ },
+ [user, partId, buildId]
+ );
+
+ const tableColumns: TableColumn[] = useMemo(() => {
+ return [
+ {
+ accessor: 'part',
+ sortable: true,
+ render: (record: any) => PartColumn(record?.part_detail)
+ },
+ {
+ accessor: 'quantity',
+ ordering: 'stock',
+ sortable: true,
+ switchable: false,
+ title: t`Build Output`,
+ render: (record: any) => {
+ let text = record.quantity;
+
+ if (record.serial) {
+ text = `# ${record.serial}`;
+ }
+
+ return (
+
+ {text}
+ {record.batch && (
+
+ {t`Batch`}: {record.batch}
+
+ )}
+
+ );
+ }
+ },
+ LocationColumn({
+ accessor: 'location_detail'
+ }),
+ {
+ accessor: 'allocations',
+ sortable: false,
+ switchable: false,
+ title: t`Allocated Items`,
+ render: (record: any) => {
+ // TODO: Implement this!
+ return '-';
+ }
+ },
+ {
+ accessor: 'tests',
+ sortable: false,
+ switchable: false,
+ title: t`Required Tests`,
+ hidden: !hasRequiredTests,
+ render: (record: any) => {
+ const extra =
+ record.results?.map((result: TestResultOverview) => {
+ return (
+ result && (
+
+ {result.result ? (
+
+ ) : (
+
+ )}
+ {result.name}
+
+ )
+ );
+ }) ?? [];
+
+ return (
+
+ }
+ extra={extra}
+ title={t`Test Results`}
+ />
+ );
+ }
+ }
+ ];
+ }, [buildId, partId]);
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/src/frontend/src/tables/company/CompanyTable.tsx b/src/frontend/src/tables/company/CompanyTable.tsx
index 9199a2248c..c364258412 100644
--- a/src/frontend/src/tables/company/CompanyTable.tsx
+++ b/src/frontend/src/tables/company/CompanyTable.tsx
@@ -1,7 +1,7 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { access } from 'fs';
-import { useMemo } from 'react';
+import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton';
@@ -10,13 +10,17 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { companyFields } from '../../forms/CompanyForms';
-import { useCreateApiFormModal } from '../../hooks/UseForm';
+import {
+ useCreateApiFormModal,
+ useEditApiFormModal
+} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
+import { RowEditAction } from '../RowActions';
/**
* A table which displays a list of company records,
@@ -68,17 +72,21 @@ export function CompanyTable({
const newCompany = useCreateApiFormModal({
url: ApiEndpoints.company_list,
- title: t`New Company`,
+ title: t`Add Company`,
fields: companyFields(),
initialData: params,
- onFormSuccess: (response) => {
- if (response.pk) {
- let base = path ?? 'company';
- navigate(`/${base}/${response.pk}`);
- } else {
- table.refreshTable();
- }
- }
+ follow: true,
+ modelType: ModelType.company
+ });
+
+ const [selectedCompany, setSelectedCompany] = useState(0);
+
+ const editCompany = useEditApiFormModal({
+ url: ApiEndpoints.company_list,
+ pk: selectedCompany,
+ title: t`Edit Company`,
+ fields: companyFields(),
+ onFormSuccess: (record: any) => table.updateRecord(record)
});
const tableFilters: TableFilter[] = useMemo(() => {
@@ -120,9 +128,27 @@ export function CompanyTable({
];
}, [user]);
+ const rowActions = useCallback(
+ (record: any) => {
+ return [
+ RowEditAction({
+ hidden:
+ !user.hasChangeRole(UserRoles.purchase_order) &&
+ !user.hasChangeRole(UserRoles.sales_order),
+ onClick: () => {
+ setSelectedCompany(record.pk);
+ editCompany.open();
+ }
+ })
+ ];
+ },
+ [user]
+ );
+
return (
<>
{newCompany.modal}
+ {editCompany.modal}
{
if (row.pk) {
let base = path ?? 'company';
diff --git a/src/frontend/src/tables/machine/MachineListTable.tsx b/src/frontend/src/tables/machine/MachineListTable.tsx
index d46772f4b2..f021197400 100644
--- a/src/frontend/src/tables/machine/MachineListTable.tsx
+++ b/src/frontend/src/tables/machine/MachineListTable.tsx
@@ -491,7 +491,7 @@ export function MachineListTable({
}, [machineDrivers, createFormMachineType]);
const createMachineForm = useCreateApiFormModal({
- title: t`Create machine`,
+ title: t`Add machine`,
url: ApiEndpoints.machine_list,
fields: {
name: {},
diff --git a/src/frontend/src/tables/part/PartTable.tsx b/src/frontend/src/tables/part/PartTable.tsx
index f9d46cad23..1418f8fc5f 100644
--- a/src/frontend/src/tables/part/PartTable.tsx
+++ b/src/frontend/src/tables/part/PartTable.tsx
@@ -278,11 +278,8 @@ export function PartListTable({ props }: { props: InvenTreeTableProps }) {
initialData: {
...(props.params ?? {})
},
- onFormSuccess: (data: any) => {
- if (data.pk) {
- navigate(getDetailUrl(ModelType.part, data.pk));
- }
- }
+ follow: true,
+ modelType: ModelType.part
});
const tableActions = useMemo(() => {
diff --git a/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx b/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx
index 3c8cafe387..fb91b1c74d 100644
--- a/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx
+++ b/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx
@@ -60,7 +60,7 @@ export function ManufacturerPartTable({ params }: { params: any }): ReactNode {
const createManufacturerPart = useCreateApiFormModal({
url: ApiEndpoints.manufacturer_part_list,
- title: t`Create Manufacturer Part`,
+ title: t`Add Manufacturer Part`,
fields: useManufacturerPartFields(),
onFormSuccess: table.refreshTable,
initialData: {
diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx
index 2dcf9426da..dd0df11d7f 100644
--- a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx
+++ b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx
@@ -1,6 +1,5 @@
import { t } from '@lingui/macro';
import { useMemo } from 'react';
-import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { Thumbnail } from '../../components/images/Thumbnail';
@@ -9,7 +8,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
-import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
@@ -43,8 +41,6 @@ export function PurchaseOrderTable({
supplierId?: number;
supplierPartId?: number;
}) {
- const navigate = useNavigate();
-
const table = useTable('purchase-order');
const user = useUserState();
@@ -115,13 +111,8 @@ export function PurchaseOrderTable({
initialData: {
supplier: supplierId
},
- onFormSuccess: (response) => {
- if (response.pk) {
- navigate(getDetailUrl(ModelType.purchaseorder, response.pk));
- } else {
- table.refreshTable();
- }
- }
+ follow: true,
+ modelType: ModelType.purchaseorder
});
const tableActions = useMemo(() => {
diff --git a/src/frontend/src/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/tables/sales/ReturnOrderTable.tsx
index c9c98564d5..73bcbfed3f 100644
--- a/src/frontend/src/tables/sales/ReturnOrderTable.tsx
+++ b/src/frontend/src/tables/sales/ReturnOrderTable.tsx
@@ -1,6 +1,5 @@
import { t } from '@lingui/macro';
-import { useCallback, useMemo } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useMemo } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { Thumbnail } from '../../components/images/Thumbnail';
@@ -9,8 +8,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useReturnOrderFields } from '../../forms/SalesOrderForms';
-import { notYetImplemented } from '../../functions/notifications';
-import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
@@ -37,7 +34,6 @@ import { InvenTreeTable } from '../InvenTreeTable';
export function ReturnOrderTable({ params }: { params?: any }) {
const table = useTable('return-orders');
const user = useUserState();
- const navigate = useNavigate();
const tableFilters: TableFilter[] = useMemo(() => {
return [
@@ -101,13 +97,8 @@ export function ReturnOrderTable({ params }: { params?: any }) {
url: ApiEndpoints.return_order_list,
title: t`Add Return Order`,
fields: returnOrderFields,
- onFormSuccess: (response) => {
- if (response.pk) {
- navigate(getDetailUrl(ModelType.returnorder, response.pk));
- } else {
- table.refreshTable();
- }
- }
+ follow: true,
+ modelType: ModelType.returnorder
});
const tableActions = useMemo(() => {
diff --git a/src/frontend/src/tables/sales/SalesOrderTable.tsx b/src/frontend/src/tables/sales/SalesOrderTable.tsx
index a2ede2f2b6..c3556e6278 100644
--- a/src/frontend/src/tables/sales/SalesOrderTable.tsx
+++ b/src/frontend/src/tables/sales/SalesOrderTable.tsx
@@ -1,6 +1,5 @@
import { t } from '@lingui/macro';
import { useMemo } from 'react';
-import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { Thumbnail } from '../../components/images/Thumbnail';
@@ -9,7 +8,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useSalesOrderFields } from '../../forms/SalesOrderForms';
-import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
@@ -43,8 +41,6 @@ export function SalesOrderTable({
const table = useTable('sales-order');
const user = useUserState();
- const navigate = useNavigate();
-
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
@@ -70,13 +66,8 @@ export function SalesOrderTable({
initialData: {
customer: customerId
},
- onFormSuccess: (response) => {
- if (response.pk) {
- navigate(getDetailUrl(ModelType.salesorder, response.pk));
- } else {
- table.refreshTable();
- }
- }
+ follow: true,
+ modelType: ModelType.salesorder
});
const tableActions = useMemo(() => {
diff --git a/src/frontend/src/tables/settings/TemplateTable.tsx b/src/frontend/src/tables/settings/TemplateTable.tsx
index c834ded101..b4caf9cb92 100644
--- a/src/frontend/src/tables/settings/TemplateTable.tsx
+++ b/src/frontend/src/tables/settings/TemplateTable.tsx
@@ -240,7 +240,7 @@ export function TemplateTable({
const newTemplate = useCreateApiFormModal({
url: apiEndpoint,
pathParams: { variant },
- title: t`Create new` + ' ' + templateTypeTranslation,
+ title: t`Add new` + ' ' + templateTypeTranslation,
fields: {
name: {},
description: {},
diff --git a/src/frontend/src/tables/stock/StockItemTable.tsx b/src/frontend/src/tables/stock/StockItemTable.tsx
index 3ac78560a2..69295942c2 100644
--- a/src/frontend/src/tables/stock/StockItemTable.tsx
+++ b/src/frontend/src/tables/stock/StockItemTable.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 { ActionDropdown } from '../../components/items/ActionDropdown';
@@ -22,14 +21,15 @@ import {
useTransferStockItem
} from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons';
-import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { TableColumn } from '../Column';
import {
+ DateColumn,
DescriptionColumn,
+ LocationColumn,
PartColumn,
StatusColumn
} from '../ColumnRenderers';
@@ -55,7 +55,7 @@ function stockItemTableColumns(): TableColumn[] {
ordering: 'stock',
sortable: true,
title: t`Stock`,
- render: (record) => {
+ render: (record: any) => {
// TODO: Push this out into a custom renderer
let quantity = record?.quantity ?? 0;
let allocated = record?.allocated ?? 0;
@@ -198,16 +198,14 @@ function stockItemTableColumns(): TableColumn[] {
accessor: 'batch',
sortable: true
},
- {
- accessor: 'location',
- sortable: true,
- render: function (record: any) {
- // TODO: Custom renderer for location
- // TODO: Note, if not "In stock" we don't want to display the actual location here
- return record?.location_detail?.pathstring ?? record.location ?? '-';
- }
- },
- // TODO: stocktake column
+ LocationColumn({
+ accessor: 'location_detail'
+ }),
+ DateColumn({
+ accessor: 'stocktake_date',
+ title: t`Stocktake`,
+ sortable: true
+ }),
{
accessor: 'expiry_date',
sortable: true,
@@ -357,8 +355,6 @@ export function StockItemTable({
const table = useTable(tableName);
const user = useUserState();
- const navigate = useNavigate();
-
const tableActionParams: StockOperationProps = useMemo(() => {
return {
items: table.selectedRecords,
@@ -377,11 +373,8 @@ export function StockItemTable({
part: params.part,
location: params.location
},
- onFormSuccess: (data: any) => {
- if (data.pk) {
- navigate(getDetailUrl(ModelType.stockitem, data.pk));
- }
- }
+ follow: true,
+ modelType: ModelType.stockitem
});
const transferStock = useTransferStockItem(tableActionParams);