diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 0b7dba4cc9..32f6ebfc35 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -140,7 +140,7 @@ ALLOWED_HOSTS = get_setting(
# Cross Origin Resource Sharing (CORS) options
# Only allow CORS access to API and media endpoints
-CORS_URLS_REGEX = r'^/(api|media)/.*$'
+CORS_URLS_REGEX = r'^/(api|media|static)/.*$'
# Extract CORS options from configuration file
CORS_ORIGIN_ALLOW_ALL = get_boolean_setting(
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index b69b090d04..cd3c18686f 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -1509,6 +1509,13 @@ class PartParameterList(PartParameterAPIMixin, ListCreateAPI):
'data': ['data_numeric', 'data'],
}
+ search_fields = [
+ 'data',
+ 'template__name',
+ 'template__description',
+ 'template__units',
+ ]
+
filterset_fields = [
'part',
'template',
diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx
index d40610c816..a0670ad1f5 100644
--- a/src/frontend/src/components/forms/ApiForm.tsx
+++ b/src/frontend/src/components/forms/ApiForm.tsx
@@ -207,6 +207,7 @@ export function ApiForm({
// Data validation error
form.setErrors(error.response.data);
setNonFieldErrors(error.response.data.non_field_errors ?? []);
+ setIsLoading(false);
break;
default:
// Unexpected state on form error
diff --git a/src/frontend/src/components/items/YesNoButton.tsx b/src/frontend/src/components/items/YesNoButton.tsx
new file mode 100644
index 0000000000..a8aed94c16
--- /dev/null
+++ b/src/frontend/src/components/items/YesNoButton.tsx
@@ -0,0 +1,18 @@
+import { t } from '@lingui/macro';
+import { Badge } from '@mantine/core';
+
+export function YesNoButton(value: any) {
+ const bool =
+ String(value).toLowerCase().trim() in ['true', '1', 't', 'y', 'yes'];
+
+ return (
+
+ {bool ? t`Yes` : t`No`}
+
+ );
+}
diff --git a/src/frontend/src/components/nav/PanelGroup.tsx b/src/frontend/src/components/nav/PanelGroup.tsx
index 89dfa17149..58e485c43f 100644
--- a/src/frontend/src/components/nav/PanelGroup.tsx
+++ b/src/frontend/src/components/nav/PanelGroup.tsx
@@ -1,4 +1,12 @@
-import { Divider, Paper, Stack, Tabs, Tooltip } from '@mantine/core';
+import {
+ ActionIcon,
+ Divider,
+ Paper,
+ Stack,
+ Tabs,
+ Tooltip
+} from '@mantine/core';
+import { useLocalStorage } from '@mantine/hooks';
import {
IconLayoutSidebarLeftCollapse,
IconLayoutSidebarRightCollapse
@@ -29,29 +37,31 @@ export type PanelType = {
* @returns
*/
export function PanelGroup({
+ pageKey,
panels,
selectedPanel,
onPanelChange
}: {
+ pageKey: string;
panels: PanelType[];
selectedPanel?: string;
onPanelChange?: (panel: string) => void;
}): ReactNode {
- // Default to the provided panel name, or the first panel
- const [activePanelName, setActivePanelName] = useState(
- selectedPanel || panels.length > 0 ? panels[0].name : ''
- );
+ const [activePanel, setActivePanel] = useLocalStorage({
+ key: `panel-group-active-panel-${pageKey}`,
+ defaultValue: selectedPanel || panels.length > 0 ? panels[0].name : ''
+ });
// Update the active panel when the selected panel changes
useEffect(() => {
if (selectedPanel) {
- setActivePanelName(selectedPanel);
+ setActivePanel(selectedPanel);
}
}, [selectedPanel]);
// Callback when the active panel changes
function handlePanelChange(panel: string) {
- setActivePanelName(panel);
+ setActivePanel(panel);
// Optionally call external callback hook
if (onPanelChange) {
@@ -64,7 +74,7 @@ export function PanelGroup({
return (
)
)}
- setExpanded(!expanded)}
- icon={
- expanded ? (
-
- ) : (
-
- )
- }
- />
+ >
+ {expanded ? (
+
+ ) : (
+
+ )}
+
{panels.map(
(panel, idx) =>
diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx
index e310c48f57..47c6222bed 100644
--- a/src/frontend/src/components/render/Instance.tsx
+++ b/src/frontend/src/components/render/Instance.tsx
@@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
-import { Alert } from '@mantine/core';
+import { Alert, Space } from '@mantine/core';
import { Group, Text } from '@mantine/core';
import { ReactNode } from 'react';
@@ -18,7 +18,11 @@ import {
RenderSalesOrder,
RenderSalesOrderShipment
} from './Order';
-import { RenderPart, RenderPartCategory } from './Part';
+import {
+ RenderPart,
+ RenderPartCategory,
+ RenderPartParameterTemplate
+} from './Part';
import { RenderStockItem, RenderStockLocation } from './Stock';
import { RenderOwner, RenderUser } from './User';
@@ -40,6 +44,7 @@ const RendererLookup: EnumDictionary<
[ModelType.owner]: RenderOwner,
[ModelType.part]: RenderPart,
[ModelType.partcategory]: RenderPartCategory,
+ [ModelType.partparametertemplate]: RenderPartParameterTemplate,
[ModelType.purchaseorder]: RenderPurchaseOrder,
[ModelType.returnorder]: RenderReturnOrder,
[ModelType.salesorder]: RenderSalesOrder,
@@ -63,8 +68,18 @@ export function RenderInstance({
model: ModelType | undefined;
instance: any;
}): ReactNode {
- if (model === undefined) return ;
+ if (model === undefined) {
+ console.error('RenderInstance: No model provided');
+ return ;
+ }
+
const RenderComponent = RendererLookup[model];
+
+ if (!RenderComponent) {
+ console.error(`RenderInstance: No renderer for model ${model}`);
+ return ;
+ }
+
return ;
}
@@ -74,12 +89,14 @@ export function RenderInstance({
export function RenderInlineModel({
primary,
secondary,
+ suffix,
image,
labels,
url
}: {
primary: string;
secondary?: string;
+ suffix?: string;
image?: string;
labels?: string[];
url?: string;
@@ -88,10 +105,18 @@ export function RenderInlineModel({
// TODO: Handle URL
return (
-
- {image && Thumbnail({ src: image, size: 18 })}
- {primary}
- {secondary && {secondary}}
+
+
+ {image && Thumbnail({ src: image, size: 18 })}
+ {primary}
+ {secondary && {secondary}}
+
+ {suffix && (
+ <>
+
+ {suffix}
+ >
+ )}
);
}
diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx
index 17a671fd40..3a250c3f35 100644
--- a/src/frontend/src/components/render/ModelType.tsx
+++ b/src/frontend/src/components/render/ModelType.tsx
@@ -5,6 +5,7 @@ export enum ModelType {
supplierpart = 'supplierpart',
manufacturerpart = 'manufacturerpart',
partcategory = 'partcategory',
+ partparametertemplate = 'partparametertemplate',
stockitem = 'stockitem',
stocklocation = 'stocklocation',
build = 'build',
@@ -37,6 +38,12 @@ export const ModelInformationDict: ModelDictory = {
url_overview: '/part',
url_detail: '/part/:pk/'
},
+ partparametertemplate: {
+ label: t`Part Parameter Template`,
+ label_multiple: t`Part Parameter Templates`,
+ url_overview: '/partparametertemplate',
+ url_detail: '/partparametertemplate/:pk/'
+ },
supplierpart: {
label: t`Supplier Part`,
label_multiple: t`Supplier Parts`,
diff --git a/src/frontend/src/components/render/Part.tsx b/src/frontend/src/components/render/Part.tsx
index cf0ec5d282..48af44aef6 100644
--- a/src/frontend/src/components/render/Part.tsx
+++ b/src/frontend/src/components/render/Part.tsx
@@ -30,3 +30,20 @@ export function RenderPartCategory({ instance }: { instance: any }): ReactNode {
/>
);
}
+
+/**
+ * Inline rendering of a PartParameterTemplate instance
+ */
+export function RenderPartParameterTemplate({
+ instance
+}: {
+ instance: any;
+}): ReactNode {
+ return (
+
+ );
+}
diff --git a/src/frontend/src/components/tables/InvenTreeTable.tsx b/src/frontend/src/components/tables/InvenTreeTable.tsx
index deab75d8c0..922982aa1d 100644
--- a/src/frontend/src/components/tables/InvenTreeTable.tsx
+++ b/src/frontend/src/components/tables/InvenTreeTable.tsx
@@ -387,7 +387,7 @@ export function InvenTreeTable({
fetchTableData,
{
refetchOnWindowFocus: false,
- refetchOnMount: 'always'
+ refetchOnMount: true
}
);
diff --git a/src/frontend/src/components/tables/part/PartParameterTable.tsx b/src/frontend/src/components/tables/part/PartParameterTable.tsx
new file mode 100644
index 0000000000..541f663ae5
--- /dev/null
+++ b/src/frontend/src/components/tables/part/PartParameterTable.tsx
@@ -0,0 +1,170 @@
+import { t } from '@lingui/macro';
+import { ActionIcon, Text, Tooltip } from '@mantine/core';
+import { IconTextPlus } from '@tabler/icons-react';
+import { useCallback, useMemo } from 'react';
+
+import {
+ openCreateApiForm,
+ openDeleteApiForm,
+ openEditApiForm
+} from '../../../functions/forms';
+import { useTableRefresh } from '../../../hooks/TableRefresh';
+import { ApiPaths, apiUrl } from '../../../states/ApiState';
+import { YesNoButton } from '../../items/YesNoButton';
+import { TableColumn } from '../Column';
+import { InvenTreeTable } from '../InvenTreeTable';
+
+/**
+ * Construct a table listing parameters for a given part
+ */
+export function PartParameterTable({ partId }: { partId: any }) {
+ const { tableKey, refreshTable } = useTableRefresh('part-parameters');
+
+ const tableColumns: TableColumn[] = useMemo(() => {
+ return [
+ {
+ accessor: 'name',
+ title: t`Parameter`,
+ switchable: false,
+ sortable: true,
+ render: (record) => record.template_detail?.name
+ },
+ {
+ accessor: 'description',
+ title: t`Description`,
+ sortable: false,
+ switchable: true,
+ render: (record) => record.template_detail?.description
+ },
+ {
+ accessor: 'data',
+ title: t`Value`,
+ switchable: false,
+ sortable: true,
+ render: (record) => {
+ let template = record.template_detail;
+
+ if (template?.checkbox) {
+ return ;
+ }
+
+ if (record.data_numeric) {
+ // TODO: Numeric data
+ }
+
+ // TODO: Units
+
+ return record.data;
+ }
+ },
+ {
+ accessor: 'units',
+ title: t`Units`,
+ switchable: true,
+ sortable: true,
+ render: (record) => record.template_detail?.units
+ }
+ ];
+ }, []);
+
+ // Callback for row actions
+ // TODO: Adjust based on user permissions
+ const rowActions = useCallback((record: any) => {
+ let actions = [];
+
+ actions.push({
+ title: t`Edit`,
+ onClick: () => {
+ openEditApiForm({
+ name: 'edit-part-parameter',
+ url: ApiPaths.part_parameter_list,
+ pk: record.pk,
+ title: t`Edit Part Parameter`,
+ fields: {
+ part: {
+ hidden: true
+ },
+ template: {},
+ data: {}
+ },
+ successMessage: t`Part parameter updated`,
+ onFormSuccess: refreshTable
+ });
+ }
+ });
+
+ actions.push({
+ title: t`Delete`,
+ color: 'red',
+ onClick: () => {
+ openDeleteApiForm({
+ name: 'delete-part-parameter',
+ url: ApiPaths.part_parameter_list,
+ pk: record.pk,
+ title: t`Delete Part Parameter`,
+ successMessage: t`Part parameter deleted`,
+ onFormSuccess: refreshTable,
+ preFormContent: (
+ {t`Are you sure you want to remove this parameter?`}
+ )
+ });
+ }
+ });
+
+ return actions;
+ }, []);
+
+ const addParameter = useCallback(() => {
+ if (!partId) {
+ return;
+ }
+
+ openCreateApiForm({
+ name: 'add-part-parameter',
+ url: ApiPaths.part_parameter_list,
+ title: t`Add Part Parameter`,
+ fields: {
+ part: {
+ hidden: true,
+ value: partId
+ },
+ template: {},
+ data: {}
+ },
+ successMessage: t`Part parameter added`,
+ onFormSuccess: refreshTable
+ });
+ }, [partId]);
+
+ // Custom table actions
+ const tableActions = useMemo(() => {
+ let actions = [];
+
+ // TODO: Hide if user does not have permission to edit parts
+ actions.push(
+
+
+
+
+
+ );
+
+ return actions;
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/src/frontend/src/components/tables/part/PartTable.tsx b/src/frontend/src/components/tables/part/PartTable.tsx
index 49bdb5d58d..92abbf5c4e 100644
--- a/src/frontend/src/components/tables/part/PartTable.tsx
+++ b/src/frontend/src/components/tables/part/PartTable.tsx
@@ -35,11 +35,6 @@ function partTableColumns(): TableColumn[] {
/>
{record.full_name}
- //
);
}
},
@@ -68,7 +63,7 @@ function partTableColumns(): TableColumn[] {
render: function (record: any) {
// TODO: Link to the category detail page
return shortenString({
- str: record.category_detail.pathstring
+ str: record.category_detail?.pathstring
});
}
},
diff --git a/src/frontend/src/components/tables/part/RelatedPartTable.tsx b/src/frontend/src/components/tables/part/RelatedPartTable.tsx
index 4bd3c0eb71..1f87a5203b 100644
--- a/src/frontend/src/components/tables/part/RelatedPartTable.tsx
+++ b/src/frontend/src/components/tables/part/RelatedPartTable.tsx
@@ -11,6 +11,9 @@ import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
+/**
+ * Construct a table listing related parts for a given part
+ */
export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
const { tableKey, refreshTable } = useTableRefresh('relatedparts');
@@ -56,7 +59,7 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
}
}
];
- }, []);
+ }, [partId]);
const addRelatedPart = useCallback(() => {
openCreateApiForm({
@@ -75,7 +78,7 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
successMessage: t`Related part added`,
onFormSuccess: refreshTable
});
- }, []);
+ }, [partId]);
const customActions: ReactNode[] = useMemo(() => {
// TODO: Hide if user does not have permission to edit parts
diff --git a/src/frontend/src/components/tables/stock/StockItemTable.tsx b/src/frontend/src/components/tables/stock/StockItemTable.tsx
index bce8b77b82..b802dbfb90 100644
--- a/src/frontend/src/components/tables/stock/StockItemTable.tsx
+++ b/src/frontend/src/components/tables/stock/StockItemTable.tsx
@@ -1,11 +1,12 @@
import { t } from '@lingui/macro';
-import { Text } from '@mantine/core';
+import { Group, Text } from '@mantine/core';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { notYetImplemented } from '../../../functions/notifications';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
+import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column';
import { TableFilter } from '../Filter';
import { RowAction } from '../RowActions';
@@ -21,14 +22,16 @@ function stockItemTableColumns(): TableColumn[] {
sortable: true,
title: t`Part`,
render: function (record: any) {
- let part = record.part_detail;
+ let part = record.part_detail ?? {};
return (
- {part.full_name}
- //
+
+
+ {part.full_name}
+
);
}
},
diff --git a/src/frontend/src/components/tables/stock/StockLocationTable.tsx b/src/frontend/src/components/tables/stock/StockLocationTable.tsx
index cf411321e4..2ea8c562e2 100644
--- a/src/frontend/src/components/tables/stock/StockLocationTable.tsx
+++ b/src/frontend/src/components/tables/stock/StockLocationTable.tsx
@@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
+import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
@@ -44,16 +45,14 @@ export function StockLocationTable({ params = {} }: { params?: any }) {
title: t`Structural`,
switchable: true,
sortable: true,
- render: (record: any) => (record.structural ? 'Y' : 'N')
- // TODO: custom 'true / false' label,
+ render: (record: any) =>
},
{
accessor: 'external',
title: t`External`,
switchable: true,
sortable: true,
- render: (record: any) => (record.structural ? 'Y' : 'N')
- // TODO: custom 'true / false' label,
+ render: (record: any) =>
},
{
accessor: 'location_type',
diff --git a/src/frontend/src/defaults/dashboardItems.tsx b/src/frontend/src/defaults/dashboardItems.tsx
index fe56a2450c..90004f1063 100644
--- a/src/frontend/src/defaults/dashboardItems.tsx
+++ b/src/frontend/src/defaults/dashboardItems.tsx
@@ -1,116 +1,118 @@
import { t } from '@lingui/macro';
+import { ApiPaths, apiUrl } from '../states/ApiState';
+
export const dashboardItems = [
{
id: 'starred-parts',
text: t`Subscribed Parts`,
icon: 'fa-bell',
- url: 'part',
+ url: apiUrl(ApiPaths.part_list),
params: { starred: true }
},
{
id: 'starred-categories',
text: t`Subscribed Categories`,
icon: 'fa-bell',
- url: 'part/category',
+ url: apiUrl(ApiPaths.category_list),
params: { starred: true }
},
{
id: 'latest-parts',
text: t`Latest Parts`,
icon: 'fa-newspaper',
- url: 'part',
+ url: apiUrl(ApiPaths.part_list),
params: { ordering: '-creation_date', limit: 10 }
},
{
id: 'bom-validation',
text: t`BOM Waiting Validation`,
icon: 'fa-times-circle',
- url: 'part',
+ url: apiUrl(ApiPaths.part_list),
params: { bom_valid: false }
},
{
id: 'recently-updated-stock',
text: t`Recently Updated`,
icon: 'fa-clock',
- url: 'stock',
+ url: apiUrl(ApiPaths.stock_item_list),
params: { part_detail: true, ordering: '-updated', limit: 10 }
},
{
id: 'low-stock',
text: t`Low Stock`,
icon: 'fa-flag',
- url: 'part',
+ url: apiUrl(ApiPaths.part_list),
params: { low_stock: true }
},
{
id: 'depleted-stock',
text: t`Depleted Stock`,
icon: 'fa-times',
- url: 'part',
+ url: apiUrl(ApiPaths.part_list),
params: { depleted_stock: true }
},
{
id: 'stock-to-build',
text: t`Required for Build Orders`,
icon: 'fa-bullhorn',
- url: 'part',
+ url: apiUrl(ApiPaths.part_list),
params: { stock_to_build: true }
},
{
id: 'expired-stock',
text: t`Expired Stock`,
icon: 'fa-calendar-times',
- url: 'stock',
+ url: apiUrl(ApiPaths.stock_item_list),
params: { expired: true }
},
{
id: 'stale-stock',
text: t`Stale Stock`,
icon: 'fa-stopwatch',
- url: 'stock',
+ url: apiUrl(ApiPaths.stock_item_list),
params: { stale: true, expired: true }
},
{
id: 'build-pending',
text: t`Build Orders In Progress`,
icon: 'fa-cogs',
- url: 'build',
+ url: apiUrl(ApiPaths.build_order_list),
params: { active: true }
},
{
id: 'build-overdue',
text: t`Overdue Build Orders`,
icon: 'fa-calendar-times',
- url: 'build',
+ url: apiUrl(ApiPaths.build_order_list),
params: { overdue: true }
},
{
id: 'po-outstanding',
text: t`Outstanding Purchase Orders`,
icon: 'fa-sign-in-alt',
- url: 'order/po',
+ url: apiUrl(ApiPaths.purchase_order_list),
params: { supplier_detail: true, outstanding: true }
},
{
id: 'po-overdue',
text: t`Overdue Purchase Orders`,
icon: 'fa-calendar-times',
- url: 'order/po',
+ url: apiUrl(ApiPaths.purchase_order_list),
params: { supplier_detail: true, overdue: true }
},
{
id: 'so-outstanding',
text: t`Outstanding Sales Orders`,
icon: 'fa-sign-out-alt',
- url: 'order/so',
+ url: apiUrl(ApiPaths.sales_order_list),
params: { customer_detail: true, outstanding: true }
},
{
id: 'so-overdue',
text: t`Overdue Sales Orders`,
icon: 'fa-calendar-times',
- url: 'order/so',
+ url: apiUrl(ApiPaths.sales_order_list),
params: { customer_detail: true, overdue: true }
},
{
diff --git a/src/frontend/src/pages/Notifications.tsx b/src/frontend/src/pages/Notifications.tsx
index c6ac68b4a8..45f73eb878 100644
--- a/src/frontend/src/pages/Notifications.tsx
+++ b/src/frontend/src/pages/Notifications.tsx
@@ -88,7 +88,7 @@ export default function NotificationsPage() {
<>
-
+
>
);
diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx
index bf5f5ec765..05924b0dea 100644
--- a/src/frontend/src/pages/build/BuildDetail.tsx
+++ b/src/frontend/src/pages/build/BuildDetail.tsx
@@ -148,7 +148,7 @@ export default function BuildDetail() {
actions={[]}
/>
-
+
>
);
diff --git a/src/frontend/src/pages/part/CategoryDetail.tsx b/src/frontend/src/pages/part/CategoryDetail.tsx
index ea4f869d6e..5e418fb38b 100644
--- a/src/frontend/src/pages/part/CategoryDetail.tsx
+++ b/src/frontend/src/pages/part/CategoryDetail.tsx
@@ -43,7 +43,7 @@ export default function CategoryDetail({}: {}) {
{
name: 'parts',
label: t`Parts`,
- icon: ,
+ icon: ,
content: (
,
+ label: t`Part Categories`,
+ icon: ,
content: (
,
+ icon: ,
content:
}
],
@@ -95,7 +95,7 @@ export default function CategoryDetail({}: {}) {
detail={{category.name ?? 'Top level'}}
breadcrumbs={breadcrumbs}
/>
-
+
);
}
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index e903c95085..030b07260e 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -18,22 +18,21 @@ import {
IconPackages,
IconPaperclip,
IconShoppingCart,
+ IconStack2,
IconTestPipe,
IconTools,
IconTruckDelivery,
IconVersions
} from '@tabler/icons-react';
-import React from 'react';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
-import { api } from '../../App';
import { ApiImage } from '../../components/images/ApiImage';
-import { Thumbnail } from '../../components/images/Thumbnail';
import { PlaceholderPanel } from '../../components/items/Placeholder';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { AttachmentTable } from '../../components/tables/AttachmentTable';
+import { PartParameterTable } from '../../components/tables/part/PartParameterTable';
import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable';
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
@@ -67,87 +66,99 @@ export default function PartDetail() {
{
name: 'details',
label: t`Details`,
- icon: ,
+ icon: ,
content:
},
+ {
+ name: 'parameters',
+ label: t`Parameters`,
+ icon: ,
+ content:
+ },
{
name: 'stock',
label: t`Stock`,
- icon: ,
- content: partStockTab()
+ icon: ,
+ content: (
+
+ )
},
{
name: 'variants',
label: t`Variants`,
- icon: ,
+ icon: ,
hidden: !part.is_template,
content:
},
{
name: 'bom',
label: t`Bill of Materials`,
- icon: ,
+ icon: ,
hidden: !part.assembly,
content:
},
{
name: 'builds',
label: t`Build Orders`,
- icon: ,
+ icon: ,
hidden: !part.assembly && !part.component,
content:
},
{
name: 'used_in',
label: t`Used In`,
- icon: ,
+ icon: ,
hidden: !part.component,
content:
},
{
name: 'pricing',
label: t`Pricing`,
- icon: ,
+ icon: ,
content:
},
{
name: 'suppliers',
label: t`Suppliers`,
- icon: ,
+ icon: ,
hidden: !part.purchaseable,
content:
},
{
name: 'purchase_orders',
label: t`Purchase Orders`,
- icon: ,
+ icon: ,
content: ,
hidden: !part.purchaseable
},
{
name: 'sales_orders',
label: t`Sales Orders`,
- icon: ,
+ icon: ,
content: ,
hidden: !part.salable
},
{
name: 'test_templates',
label: t`Test Templates`,
- icon: ,
+ icon: ,
content: ,
hidden: !part.trackable
},
{
name: 'related_parts',
label: t`Related Parts`,
- icon: ,
+ icon: ,
content:
},
{
name: 'attachments',
label: t`Attachments`,
- icon: ,
+ icon: ,
content: (
,
- content: partNotesTab()
+ icon: ,
+ content: (
+
+ )
}
];
}, [part]);
- function partNotesTab(): React.ReactNode {
- // TODO: Set edit permission based on user permissions
- return (
-
- );
- }
-
- function partStockTab(): React.ReactNode {
- return (
-
- );
- }
-
const breadcrumbs = useMemo(
() => [
{ name: t`Parts`, url: '/part' },
@@ -239,7 +235,7 @@ export default function PartDetail() {
]}
/>
-
+
>
);
diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx
index d1587c7245..75244ef515 100644
--- a/src/frontend/src/pages/stock/LocationDetail.tsx
+++ b/src/frontend/src/pages/stock/LocationDetail.tsx
@@ -31,7 +31,7 @@ export default function Stock() {
{
name: 'stock-items',
label: t`Stock Items`,
- icon: ,
+ icon: ,
content: (
,
+ label: t`Stock Locations`,
+ icon: ,
content: (
{location.name ?? 'Top level'}}
breadcrumbs={breadcrumbs}
/>
-
+
>
);
diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx
index 026df4e1da..ecd6cdbb17 100644
--- a/src/frontend/src/pages/stock/StockDetail.tsx
+++ b/src/frontend/src/pages/stock/StockDetail.tsx
@@ -42,37 +42,37 @@ export default function StockDetail() {
{
name: 'details',
label: t`Details`,
- icon: ,
+ icon: ,
content:
},
{
name: 'tracking',
label: t`Stock Tracking`,
- icon: ,
+ icon: ,
content:
},
{
name: 'allocations',
label: t`Allocations`,
- icon: ,
+ icon: ,
content:
},
{
name: 'installed_items',
label: t`Installed Items`,
- icon: ,
+ icon: ,
content:
},
{
name: 'child_items',
label: t`Child Items`,
- icon: ,
+ icon: ,
content:
},
{
name: 'attachments',
label: t`Attachments`,
- icon: ,
+ icon: ,
content: (
,
+ icon: ,
content: (
-
+
);
}
diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx
index 024357be2d..12ad8978a1 100644
--- a/src/frontend/src/states/ApiState.tsx
+++ b/src/frontend/src/states/ApiState.tsx
@@ -49,6 +49,8 @@ export enum ApiPaths {
category_list = 'api-category-list',
related_part_list = 'api-related-part-list',
part_attachment_list = 'api-part-attachment-list',
+ part_parameter_list = 'api-part-parameter-list',
+ part_parameter_template_list = 'api-part-parameter-template-list',
// Company URLs
company_list = 'api-company-list',
@@ -111,6 +113,10 @@ export function apiEndpoint(path: ApiPaths): string {
return 'build/attachment/';
case ApiPaths.part_list:
return 'part/';
+ case ApiPaths.part_parameter_list:
+ return 'part/parameter/';
+ case ApiPaths.part_parameter_template_list:
+ return 'part/parameter/template/';
case ApiPaths.category_list:
return 'part/category/';
case ApiPaths.related_part_list: