mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[React] Part parameters table (#5709)
* Track current panel selection in local storage * Simplify part detail tabs * Fix <PanelGroup> instances * Handle missing model type for rendering * Add some more API endpoints * Add PartParameter table * Add callback to create new part parameter * Allow PartParameter list API endpoint to be searched * More PanelGroup collapse tweaks - Still requires more attention * Fix logic for related part table - Need to rebuild columns when part id changes * Further fixes for related part table * Re-implement change to PanelGroup - useLocalStorage - Change got clobbered in recent merge conflict * Add part thumbnail to StockItemTable * Add simple <YesNo> button - Can be improved later * Fix for PartTable * Allow CORS requests to /static/ endpoint * Updates to other existing tables * Update URLs for dashboard items
This commit is contained in:
parent
a9d18ba28f
commit
9705521cd2
@ -140,7 +140,7 @@ ALLOWED_HOSTS = get_setting(
|
|||||||
# Cross Origin Resource Sharing (CORS) options
|
# Cross Origin Resource Sharing (CORS) options
|
||||||
|
|
||||||
# Only allow CORS access to API and media endpoints
|
# 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
|
# Extract CORS options from configuration file
|
||||||
CORS_ORIGIN_ALLOW_ALL = get_boolean_setting(
|
CORS_ORIGIN_ALLOW_ALL = get_boolean_setting(
|
||||||
|
@ -1509,6 +1509,13 @@ class PartParameterList(PartParameterAPIMixin, ListCreateAPI):
|
|||||||
'data': ['data_numeric', 'data'],
|
'data': ['data_numeric', 'data'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
'data',
|
||||||
|
'template__name',
|
||||||
|
'template__description',
|
||||||
|
'template__units',
|
||||||
|
]
|
||||||
|
|
||||||
filterset_fields = [
|
filterset_fields = [
|
||||||
'part',
|
'part',
|
||||||
'template',
|
'template',
|
||||||
|
@ -207,6 +207,7 @@ export function ApiForm({
|
|||||||
// Data validation error
|
// Data validation error
|
||||||
form.setErrors(error.response.data);
|
form.setErrors(error.response.data);
|
||||||
setNonFieldErrors(error.response.data.non_field_errors ?? []);
|
setNonFieldErrors(error.response.data.non_field_errors ?? []);
|
||||||
|
setIsLoading(false);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Unexpected state on form error
|
// Unexpected state on form error
|
||||||
|
18
src/frontend/src/components/items/YesNoButton.tsx
Normal file
18
src/frontend/src/components/items/YesNoButton.tsx
Normal file
@ -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 (
|
||||||
|
<Badge
|
||||||
|
color={bool ? 'green' : 'red'}
|
||||||
|
variant="filled"
|
||||||
|
radius="lg"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{bool ? t`Yes` : t`No`}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
@ -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 {
|
import {
|
||||||
IconLayoutSidebarLeftCollapse,
|
IconLayoutSidebarLeftCollapse,
|
||||||
IconLayoutSidebarRightCollapse
|
IconLayoutSidebarRightCollapse
|
||||||
@ -29,29 +37,31 @@ export type PanelType = {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function PanelGroup({
|
export function PanelGroup({
|
||||||
|
pageKey,
|
||||||
panels,
|
panels,
|
||||||
selectedPanel,
|
selectedPanel,
|
||||||
onPanelChange
|
onPanelChange
|
||||||
}: {
|
}: {
|
||||||
|
pageKey: string;
|
||||||
panels: PanelType[];
|
panels: PanelType[];
|
||||||
selectedPanel?: string;
|
selectedPanel?: string;
|
||||||
onPanelChange?: (panel: string) => void;
|
onPanelChange?: (panel: string) => void;
|
||||||
}): ReactNode {
|
}): ReactNode {
|
||||||
// Default to the provided panel name, or the first panel
|
const [activePanel, setActivePanel] = useLocalStorage<string>({
|
||||||
const [activePanelName, setActivePanelName] = useState<string>(
|
key: `panel-group-active-panel-${pageKey}`,
|
||||||
selectedPanel || panels.length > 0 ? panels[0].name : ''
|
defaultValue: selectedPanel || panels.length > 0 ? panels[0].name : ''
|
||||||
);
|
});
|
||||||
|
|
||||||
// Update the active panel when the selected panel changes
|
// Update the active panel when the selected panel changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedPanel) {
|
if (selectedPanel) {
|
||||||
setActivePanelName(selectedPanel);
|
setActivePanel(selectedPanel);
|
||||||
}
|
}
|
||||||
}, [selectedPanel]);
|
}, [selectedPanel]);
|
||||||
|
|
||||||
// Callback when the active panel changes
|
// Callback when the active panel changes
|
||||||
function handlePanelChange(panel: string) {
|
function handlePanelChange(panel: string) {
|
||||||
setActivePanelName(panel);
|
setActivePanel(panel);
|
||||||
|
|
||||||
// Optionally call external callback hook
|
// Optionally call external callback hook
|
||||||
if (onPanelChange) {
|
if (onPanelChange) {
|
||||||
@ -64,7 +74,7 @@ export function PanelGroup({
|
|||||||
return (
|
return (
|
||||||
<Paper p="sm" radius="xs" shadow="xs">
|
<Paper p="sm" radius="xs" shadow="xs">
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activePanelName}
|
value={activePanel}
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
onTabChange={handlePanelChange}
|
onTabChange={handlePanelChange}
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
@ -89,19 +99,18 @@ export function PanelGroup({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<Tabs.Tab
|
<ActionIcon
|
||||||
key="panel-tab-collapse-toggle"
|
style={{
|
||||||
p="xs"
|
paddingLeft: '10px'
|
||||||
value="collapse-toggle"
|
}}
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
icon={
|
>
|
||||||
expanded ? (
|
{expanded ? (
|
||||||
<IconLayoutSidebarLeftCollapse opacity={0.35} size={18} />
|
<IconLayoutSidebarLeftCollapse opacity={0.5} />
|
||||||
) : (
|
) : (
|
||||||
<IconLayoutSidebarRightCollapse opacity={0.35} size={18} />
|
<IconLayoutSidebarRightCollapse opacity={0.5} />
|
||||||
)
|
)}
|
||||||
}
|
</ActionIcon>
|
||||||
/>
|
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
{panels.map(
|
{panels.map(
|
||||||
(panel, idx) =>
|
(panel, idx) =>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Alert } from '@mantine/core';
|
import { Alert, Space } from '@mantine/core';
|
||||||
import { Group, Text } from '@mantine/core';
|
import { Group, Text } from '@mantine/core';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
@ -18,7 +18,11 @@ import {
|
|||||||
RenderSalesOrder,
|
RenderSalesOrder,
|
||||||
RenderSalesOrderShipment
|
RenderSalesOrderShipment
|
||||||
} from './Order';
|
} from './Order';
|
||||||
import { RenderPart, RenderPartCategory } from './Part';
|
import {
|
||||||
|
RenderPart,
|
||||||
|
RenderPartCategory,
|
||||||
|
RenderPartParameterTemplate
|
||||||
|
} from './Part';
|
||||||
import { RenderStockItem, RenderStockLocation } from './Stock';
|
import { RenderStockItem, RenderStockLocation } from './Stock';
|
||||||
import { RenderOwner, RenderUser } from './User';
|
import { RenderOwner, RenderUser } from './User';
|
||||||
|
|
||||||
@ -40,6 +44,7 @@ const RendererLookup: EnumDictionary<
|
|||||||
[ModelType.owner]: RenderOwner,
|
[ModelType.owner]: RenderOwner,
|
||||||
[ModelType.part]: RenderPart,
|
[ModelType.part]: RenderPart,
|
||||||
[ModelType.partcategory]: RenderPartCategory,
|
[ModelType.partcategory]: RenderPartCategory,
|
||||||
|
[ModelType.partparametertemplate]: RenderPartParameterTemplate,
|
||||||
[ModelType.purchaseorder]: RenderPurchaseOrder,
|
[ModelType.purchaseorder]: RenderPurchaseOrder,
|
||||||
[ModelType.returnorder]: RenderReturnOrder,
|
[ModelType.returnorder]: RenderReturnOrder,
|
||||||
[ModelType.salesorder]: RenderSalesOrder,
|
[ModelType.salesorder]: RenderSalesOrder,
|
||||||
@ -63,8 +68,18 @@ export function RenderInstance({
|
|||||||
model: ModelType | undefined;
|
model: ModelType | undefined;
|
||||||
instance: any;
|
instance: any;
|
||||||
}): ReactNode {
|
}): ReactNode {
|
||||||
if (model === undefined) return <UnknownRenderer model={model} />;
|
if (model === undefined) {
|
||||||
|
console.error('RenderInstance: No model provided');
|
||||||
|
return <UnknownRenderer model={model} />;
|
||||||
|
}
|
||||||
|
|
||||||
const RenderComponent = RendererLookup[model];
|
const RenderComponent = RendererLookup[model];
|
||||||
|
|
||||||
|
if (!RenderComponent) {
|
||||||
|
console.error(`RenderInstance: No renderer for model ${model}`);
|
||||||
|
return <UnknownRenderer model={model} />;
|
||||||
|
}
|
||||||
|
|
||||||
return <RenderComponent instance={instance} />;
|
return <RenderComponent instance={instance} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,12 +89,14 @@ export function RenderInstance({
|
|||||||
export function RenderInlineModel({
|
export function RenderInlineModel({
|
||||||
primary,
|
primary,
|
||||||
secondary,
|
secondary,
|
||||||
|
suffix,
|
||||||
image,
|
image,
|
||||||
labels,
|
labels,
|
||||||
url
|
url
|
||||||
}: {
|
}: {
|
||||||
primary: string;
|
primary: string;
|
||||||
secondary?: string;
|
secondary?: string;
|
||||||
|
suffix?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
labels?: string[];
|
labels?: string[];
|
||||||
url?: string;
|
url?: string;
|
||||||
@ -88,10 +105,18 @@ export function RenderInlineModel({
|
|||||||
// TODO: Handle URL
|
// TODO: Handle URL
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group spacing="xs">
|
<Group spacing="xs" position="apart">
|
||||||
{image && Thumbnail({ src: image, size: 18 })}
|
<Group spacing="xs" position="left">
|
||||||
<Text size="sm">{primary}</Text>
|
{image && Thumbnail({ src: image, size: 18 })}
|
||||||
{secondary && <Text size="xs">{secondary}</Text>}
|
<Text size="sm">{primary}</Text>
|
||||||
|
{secondary && <Text size="xs">{secondary}</Text>}
|
||||||
|
</Group>
|
||||||
|
{suffix && (
|
||||||
|
<>
|
||||||
|
<Space />
|
||||||
|
<Text size="xs">{suffix}</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ export enum ModelType {
|
|||||||
supplierpart = 'supplierpart',
|
supplierpart = 'supplierpart',
|
||||||
manufacturerpart = 'manufacturerpart',
|
manufacturerpart = 'manufacturerpart',
|
||||||
partcategory = 'partcategory',
|
partcategory = 'partcategory',
|
||||||
|
partparametertemplate = 'partparametertemplate',
|
||||||
stockitem = 'stockitem',
|
stockitem = 'stockitem',
|
||||||
stocklocation = 'stocklocation',
|
stocklocation = 'stocklocation',
|
||||||
build = 'build',
|
build = 'build',
|
||||||
@ -37,6 +38,12 @@ export const ModelInformationDict: ModelDictory = {
|
|||||||
url_overview: '/part',
|
url_overview: '/part',
|
||||||
url_detail: '/part/:pk/'
|
url_detail: '/part/:pk/'
|
||||||
},
|
},
|
||||||
|
partparametertemplate: {
|
||||||
|
label: t`Part Parameter Template`,
|
||||||
|
label_multiple: t`Part Parameter Templates`,
|
||||||
|
url_overview: '/partparametertemplate',
|
||||||
|
url_detail: '/partparametertemplate/:pk/'
|
||||||
|
},
|
||||||
supplierpart: {
|
supplierpart: {
|
||||||
label: t`Supplier Part`,
|
label: t`Supplier Part`,
|
||||||
label_multiple: t`Supplier Parts`,
|
label_multiple: t`Supplier Parts`,
|
||||||
|
@ -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 (
|
||||||
|
<RenderInlineModel
|
||||||
|
primary={instance.name}
|
||||||
|
secondary={instance.description}
|
||||||
|
suffix={instance.units}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -387,7 +387,7 @@ export function InvenTreeTable({
|
|||||||
fetchTableData,
|
fetchTableData,
|
||||||
{
|
{
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnMount: 'always'
|
refetchOnMount: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
170
src/frontend/src/components/tables/part/PartParameterTable.tsx
Normal file
170
src/frontend/src/components/tables/part/PartParameterTable.tsx
Normal file
@ -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 <YesNoButton value={record.data} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: (
|
||||||
|
<Text>{t`Are you sure you want to remove this parameter?`}</Text>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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(
|
||||||
|
<Tooltip label={t`Add parameter`}>
|
||||||
|
<ActionIcon radius="sm" onClick={addParameter}>
|
||||||
|
<IconTextPlus color="green" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InvenTreeTable
|
||||||
|
url={apiUrl(ApiPaths.part_parameter_list)}
|
||||||
|
tableKey={tableKey}
|
||||||
|
columns={tableColumns}
|
||||||
|
props={{
|
||||||
|
rowActions: rowActions,
|
||||||
|
customActionGroups: tableActions,
|
||||||
|
params: {
|
||||||
|
part: partId,
|
||||||
|
template_detail: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -35,11 +35,6 @@ function partTableColumns(): TableColumn[] {
|
|||||||
/>
|
/>
|
||||||
<Text>{record.full_name}</Text>
|
<Text>{record.full_name}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
// <ThumbnailHoverCard
|
|
||||||
// src={record.thumbnail || record.image}
|
|
||||||
// text={record.name}
|
|
||||||
// link=""
|
|
||||||
// />
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -68,7 +63,7 @@ function partTableColumns(): TableColumn[] {
|
|||||||
render: function (record: any) {
|
render: function (record: any) {
|
||||||
// TODO: Link to the category detail page
|
// TODO: Link to the category detail page
|
||||||
return shortenString({
|
return shortenString({
|
||||||
str: record.category_detail.pathstring
|
str: record.category_detail?.pathstring
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -11,6 +11,9 @@ import { Thumbnail } from '../../images/Thumbnail';
|
|||||||
import { TableColumn } from '../Column';
|
import { TableColumn } from '../Column';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a table listing related parts for a given part
|
||||||
|
*/
|
||||||
export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
|
export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
|
||||||
const { tableKey, refreshTable } = useTableRefresh('relatedparts');
|
const { tableKey, refreshTable } = useTableRefresh('relatedparts');
|
||||||
|
|
||||||
@ -56,7 +59,7 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, []);
|
}, [partId]);
|
||||||
|
|
||||||
const addRelatedPart = useCallback(() => {
|
const addRelatedPart = useCallback(() => {
|
||||||
openCreateApiForm({
|
openCreateApiForm({
|
||||||
@ -75,7 +78,7 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
|
|||||||
successMessage: t`Related part added`,
|
successMessage: t`Related part added`,
|
||||||
onFormSuccess: refreshTable
|
onFormSuccess: refreshTable
|
||||||
});
|
});
|
||||||
}, []);
|
}, [partId]);
|
||||||
|
|
||||||
const customActions: ReactNode[] = useMemo(() => {
|
const customActions: ReactNode[] = useMemo(() => {
|
||||||
// TODO: Hide if user does not have permission to edit parts
|
// TODO: Hide if user does not have permission to edit parts
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Text } from '@mantine/core';
|
import { Group, Text } from '@mantine/core';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { notYetImplemented } from '../../../functions/notifications';
|
import { notYetImplemented } from '../../../functions/notifications';
|
||||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||||
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||||
|
import { Thumbnail } from '../../images/Thumbnail';
|
||||||
import { TableColumn } from '../Column';
|
import { TableColumn } from '../Column';
|
||||||
import { TableFilter } from '../Filter';
|
import { TableFilter } from '../Filter';
|
||||||
import { RowAction } from '../RowActions';
|
import { RowAction } from '../RowActions';
|
||||||
@ -21,14 +22,16 @@ function stockItemTableColumns(): TableColumn[] {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
title: t`Part`,
|
title: t`Part`,
|
||||||
render: function (record: any) {
|
render: function (record: any) {
|
||||||
let part = record.part_detail;
|
let part = record.part_detail ?? {};
|
||||||
return (
|
return (
|
||||||
<Text>{part.full_name}</Text>
|
<Group spacing="xs" noWrap={true}>
|
||||||
// <ThumbnailHoverCard
|
<Thumbnail
|
||||||
// src={part.thumbnail || part.image}
|
src={part?.thumbnail || part?.image}
|
||||||
// text={part.name}
|
alt={part?.name}
|
||||||
// link=""
|
size={24}
|
||||||
// />
|
/>
|
||||||
|
<Text>{part.full_name}</Text>
|
||||||
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
|
|
||||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||||
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||||
|
import { YesNoButton } from '../../items/YesNoButton';
|
||||||
import { TableColumn } from '../Column';
|
import { TableColumn } from '../Column';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
@ -44,16 +45,14 @@ export function StockLocationTable({ params = {} }: { params?: any }) {
|
|||||||
title: t`Structural`,
|
title: t`Structural`,
|
||||||
switchable: true,
|
switchable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (record: any) => (record.structural ? 'Y' : 'N')
|
render: (record: any) => <YesNoButton value={record.structural} />
|
||||||
// TODO: custom 'true / false' label,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'external',
|
accessor: 'external',
|
||||||
title: t`External`,
|
title: t`External`,
|
||||||
switchable: true,
|
switchable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (record: any) => (record.structural ? 'Y' : 'N')
|
render: (record: any) => <YesNoButton value={record.external} />
|
||||||
// TODO: custom 'true / false' label,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'location_type',
|
accessor: 'location_type',
|
||||||
|
@ -1,116 +1,118 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { ApiPaths, apiUrl } from '../states/ApiState';
|
||||||
|
|
||||||
export const dashboardItems = [
|
export const dashboardItems = [
|
||||||
{
|
{
|
||||||
id: 'starred-parts',
|
id: 'starred-parts',
|
||||||
text: t`Subscribed Parts`,
|
text: t`Subscribed Parts`,
|
||||||
icon: 'fa-bell',
|
icon: 'fa-bell',
|
||||||
url: 'part',
|
url: apiUrl(ApiPaths.part_list),
|
||||||
params: { starred: true }
|
params: { starred: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'starred-categories',
|
id: 'starred-categories',
|
||||||
text: t`Subscribed Categories`,
|
text: t`Subscribed Categories`,
|
||||||
icon: 'fa-bell',
|
icon: 'fa-bell',
|
||||||
url: 'part/category',
|
url: apiUrl(ApiPaths.category_list),
|
||||||
params: { starred: true }
|
params: { starred: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'latest-parts',
|
id: 'latest-parts',
|
||||||
text: t`Latest Parts`,
|
text: t`Latest Parts`,
|
||||||
icon: 'fa-newspaper',
|
icon: 'fa-newspaper',
|
||||||
url: 'part',
|
url: apiUrl(ApiPaths.part_list),
|
||||||
params: { ordering: '-creation_date', limit: 10 }
|
params: { ordering: '-creation_date', limit: 10 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bom-validation',
|
id: 'bom-validation',
|
||||||
text: t`BOM Waiting Validation`,
|
text: t`BOM Waiting Validation`,
|
||||||
icon: 'fa-times-circle',
|
icon: 'fa-times-circle',
|
||||||
url: 'part',
|
url: apiUrl(ApiPaths.part_list),
|
||||||
params: { bom_valid: false }
|
params: { bom_valid: false }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'recently-updated-stock',
|
id: 'recently-updated-stock',
|
||||||
text: t`Recently Updated`,
|
text: t`Recently Updated`,
|
||||||
icon: 'fa-clock',
|
icon: 'fa-clock',
|
||||||
url: 'stock',
|
url: apiUrl(ApiPaths.stock_item_list),
|
||||||
params: { part_detail: true, ordering: '-updated', limit: 10 }
|
params: { part_detail: true, ordering: '-updated', limit: 10 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'low-stock',
|
id: 'low-stock',
|
||||||
text: t`Low Stock`,
|
text: t`Low Stock`,
|
||||||
icon: 'fa-flag',
|
icon: 'fa-flag',
|
||||||
url: 'part',
|
url: apiUrl(ApiPaths.part_list),
|
||||||
params: { low_stock: true }
|
params: { low_stock: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'depleted-stock',
|
id: 'depleted-stock',
|
||||||
text: t`Depleted Stock`,
|
text: t`Depleted Stock`,
|
||||||
icon: 'fa-times',
|
icon: 'fa-times',
|
||||||
url: 'part',
|
url: apiUrl(ApiPaths.part_list),
|
||||||
params: { depleted_stock: true }
|
params: { depleted_stock: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stock-to-build',
|
id: 'stock-to-build',
|
||||||
text: t`Required for Build Orders`,
|
text: t`Required for Build Orders`,
|
||||||
icon: 'fa-bullhorn',
|
icon: 'fa-bullhorn',
|
||||||
url: 'part',
|
url: apiUrl(ApiPaths.part_list),
|
||||||
params: { stock_to_build: true }
|
params: { stock_to_build: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'expired-stock',
|
id: 'expired-stock',
|
||||||
text: t`Expired Stock`,
|
text: t`Expired Stock`,
|
||||||
icon: 'fa-calendar-times',
|
icon: 'fa-calendar-times',
|
||||||
url: 'stock',
|
url: apiUrl(ApiPaths.stock_item_list),
|
||||||
params: { expired: true }
|
params: { expired: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stale-stock',
|
id: 'stale-stock',
|
||||||
text: t`Stale Stock`,
|
text: t`Stale Stock`,
|
||||||
icon: 'fa-stopwatch',
|
icon: 'fa-stopwatch',
|
||||||
url: 'stock',
|
url: apiUrl(ApiPaths.stock_item_list),
|
||||||
params: { stale: true, expired: true }
|
params: { stale: true, expired: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'build-pending',
|
id: 'build-pending',
|
||||||
text: t`Build Orders In Progress`,
|
text: t`Build Orders In Progress`,
|
||||||
icon: 'fa-cogs',
|
icon: 'fa-cogs',
|
||||||
url: 'build',
|
url: apiUrl(ApiPaths.build_order_list),
|
||||||
params: { active: true }
|
params: { active: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'build-overdue',
|
id: 'build-overdue',
|
||||||
text: t`Overdue Build Orders`,
|
text: t`Overdue Build Orders`,
|
||||||
icon: 'fa-calendar-times',
|
icon: 'fa-calendar-times',
|
||||||
url: 'build',
|
url: apiUrl(ApiPaths.build_order_list),
|
||||||
params: { overdue: true }
|
params: { overdue: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'po-outstanding',
|
id: 'po-outstanding',
|
||||||
text: t`Outstanding Purchase Orders`,
|
text: t`Outstanding Purchase Orders`,
|
||||||
icon: 'fa-sign-in-alt',
|
icon: 'fa-sign-in-alt',
|
||||||
url: 'order/po',
|
url: apiUrl(ApiPaths.purchase_order_list),
|
||||||
params: { supplier_detail: true, outstanding: true }
|
params: { supplier_detail: true, outstanding: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'po-overdue',
|
id: 'po-overdue',
|
||||||
text: t`Overdue Purchase Orders`,
|
text: t`Overdue Purchase Orders`,
|
||||||
icon: 'fa-calendar-times',
|
icon: 'fa-calendar-times',
|
||||||
url: 'order/po',
|
url: apiUrl(ApiPaths.purchase_order_list),
|
||||||
params: { supplier_detail: true, overdue: true }
|
params: { supplier_detail: true, overdue: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'so-outstanding',
|
id: 'so-outstanding',
|
||||||
text: t`Outstanding Sales Orders`,
|
text: t`Outstanding Sales Orders`,
|
||||||
icon: 'fa-sign-out-alt',
|
icon: 'fa-sign-out-alt',
|
||||||
url: 'order/so',
|
url: apiUrl(ApiPaths.sales_order_list),
|
||||||
params: { customer_detail: true, outstanding: true }
|
params: { customer_detail: true, outstanding: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'so-overdue',
|
id: 'so-overdue',
|
||||||
text: t`Overdue Sales Orders`,
|
text: t`Overdue Sales Orders`,
|
||||||
icon: 'fa-calendar-times',
|
icon: 'fa-calendar-times',
|
||||||
url: 'order/so',
|
url: apiUrl(ApiPaths.sales_order_list),
|
||||||
params: { customer_detail: true, overdue: true }
|
params: { customer_detail: true, overdue: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -88,7 +88,7 @@ export default function NotificationsPage() {
|
|||||||
<>
|
<>
|
||||||
<Stack>
|
<Stack>
|
||||||
<PageDetail title={t`Notifications`} />
|
<PageDetail title={t`Notifications`} />
|
||||||
<PanelGroup panels={notificationPanels} />
|
<PanelGroup pageKey="notifications" panels={notificationPanels} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -148,7 +148,7 @@ export default function BuildDetail() {
|
|||||||
actions={[<PlaceholderPill key="1" />]}
|
actions={[<PlaceholderPill key="1" />]}
|
||||||
/>
|
/>
|
||||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||||
<PanelGroup panels={buildPanels} />
|
<PanelGroup pageKey="build" panels={buildPanels} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -43,7 +43,7 @@ export default function CategoryDetail({}: {}) {
|
|||||||
{
|
{
|
||||||
name: 'parts',
|
name: 'parts',
|
||||||
label: t`Parts`,
|
label: t`Parts`,
|
||||||
icon: <IconCategory size="18" />,
|
icon: <IconCategory />,
|
||||||
content: (
|
content: (
|
||||||
<PartListTable
|
<PartListTable
|
||||||
props={{
|
props={{
|
||||||
@ -56,8 +56,8 @@ export default function CategoryDetail({}: {}) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'subcategories',
|
name: 'subcategories',
|
||||||
label: t`Subcategories`,
|
label: t`Part Categories`,
|
||||||
icon: <IconSitemap size="18" />,
|
icon: <IconSitemap />,
|
||||||
content: (
|
content: (
|
||||||
<PartCategoryTable
|
<PartCategoryTable
|
||||||
params={{
|
params={{
|
||||||
@ -69,7 +69,7 @@ export default function CategoryDetail({}: {}) {
|
|||||||
{
|
{
|
||||||
name: 'parameters',
|
name: 'parameters',
|
||||||
label: t`Parameters`,
|
label: t`Parameters`,
|
||||||
icon: <IconListDetails size="18" />,
|
icon: <IconListDetails />,
|
||||||
content: <PlaceholderPanel />
|
content: <PlaceholderPanel />
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -95,7 +95,7 @@ export default function CategoryDetail({}: {}) {
|
|||||||
detail={<Text>{category.name ?? 'Top level'}</Text>}
|
detail={<Text>{category.name ?? 'Top level'}</Text>}
|
||||||
breadcrumbs={breadcrumbs}
|
breadcrumbs={breadcrumbs}
|
||||||
/>
|
/>
|
||||||
<PanelGroup panels={categoryPanels} />
|
<PanelGroup pageKey="partcategory" panels={categoryPanels} />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -18,22 +18,21 @@ import {
|
|||||||
IconPackages,
|
IconPackages,
|
||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
IconShoppingCart,
|
IconShoppingCart,
|
||||||
|
IconStack2,
|
||||||
IconTestPipe,
|
IconTestPipe,
|
||||||
IconTools,
|
IconTools,
|
||||||
IconTruckDelivery,
|
IconTruckDelivery,
|
||||||
IconVersions
|
IconVersions
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import React from 'react';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { api } from '../../App';
|
|
||||||
import { ApiImage } from '../../components/images/ApiImage';
|
import { ApiImage } from '../../components/images/ApiImage';
|
||||||
import { Thumbnail } from '../../components/images/Thumbnail';
|
|
||||||
import { PlaceholderPanel } from '../../components/items/Placeholder';
|
import { PlaceholderPanel } from '../../components/items/Placeholder';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||||
import { AttachmentTable } from '../../components/tables/AttachmentTable';
|
import { AttachmentTable } from '../../components/tables/AttachmentTable';
|
||||||
|
import { PartParameterTable } from '../../components/tables/part/PartParameterTable';
|
||||||
import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable';
|
import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable';
|
||||||
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
|
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
|
||||||
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
||||||
@ -67,87 +66,99 @@ export default function PartDetail() {
|
|||||||
{
|
{
|
||||||
name: 'details',
|
name: 'details',
|
||||||
label: t`Details`,
|
label: t`Details`,
|
||||||
icon: <IconInfoCircle size="18" />,
|
icon: <IconInfoCircle />,
|
||||||
content: <PlaceholderPanel />
|
content: <PlaceholderPanel />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'parameters',
|
||||||
|
label: t`Parameters`,
|
||||||
|
icon: <IconList />,
|
||||||
|
content: <PartParameterTable partId={id ?? -1} />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'stock',
|
name: 'stock',
|
||||||
label: t`Stock`,
|
label: t`Stock`,
|
||||||
icon: <IconPackages size="18" />,
|
icon: <IconPackages />,
|
||||||
content: partStockTab()
|
content: (
|
||||||
|
<StockItemTable
|
||||||
|
params={{
|
||||||
|
part: part.pk ?? -1
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'variants',
|
name: 'variants',
|
||||||
label: t`Variants`,
|
label: t`Variants`,
|
||||||
icon: <IconVersions size="18" />,
|
icon: <IconVersions />,
|
||||||
hidden: !part.is_template,
|
hidden: !part.is_template,
|
||||||
content: <PlaceholderPanel />
|
content: <PlaceholderPanel />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'bom',
|
name: 'bom',
|
||||||
label: t`Bill of Materials`,
|
label: t`Bill of Materials`,
|
||||||
icon: <IconListTree size="18" />,
|
icon: <IconListTree />,
|
||||||
hidden: !part.assembly,
|
hidden: !part.assembly,
|
||||||
content: <PlaceholderPanel />
|
content: <PlaceholderPanel />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'builds',
|
name: 'builds',
|
||||||
label: t`Build Orders`,
|
label: t`Build Orders`,
|
||||||
icon: <IconTools size="18" />,
|
icon: <IconTools />,
|
||||||
hidden: !part.assembly && !part.component,
|
hidden: !part.assembly && !part.component,
|
||||||
content: <PlaceholderPanel />
|
content: <PlaceholderPanel />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'used_in',
|
name: 'used_in',
|
||||||
label: t`Used In`,
|
label: t`Used In`,
|
||||||
icon: <IconList size="18" />,
|
icon: <IconStack2 />,
|
||||||
hidden: !part.component,
|
hidden: !part.component,
|
||||||
content: <PlaceholderPanel />
|
content: <PlaceholderPanel />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'pricing',
|
name: 'pricing',
|
||||||
label: t`Pricing`,
|
label: t`Pricing`,
|
||||||
icon: <IconCurrencyDollar size="18" />,
|
icon: <IconCurrencyDollar />,
|
||||||
content: <PlaceholderPanel />
|
content: <PlaceholderPanel />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'suppliers',
|
name: 'suppliers',
|
||||||
label: t`Suppliers`,
|
label: t`Suppliers`,
|
||||||
icon: <IconBuilding size="18" />,
|
icon: <IconBuilding />,
|
||||||
hidden: !part.purchaseable,
|
hidden: !part.purchaseable,
|
||||||
content: <PlaceholderPanel />
|
content: <PlaceholderPanel />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'purchase_orders',
|
name: 'purchase_orders',
|
||||||
label: t`Purchase Orders`,
|
label: t`Purchase Orders`,
|
||||||
icon: <IconShoppingCart size="18" />,
|
icon: <IconShoppingCart />,
|
||||||
content: <PlaceholderPanel />,
|
content: <PlaceholderPanel />,
|
||||||
hidden: !part.purchaseable
|
hidden: !part.purchaseable
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'sales_orders',
|
name: 'sales_orders',
|
||||||
label: t`Sales Orders`,
|
label: t`Sales Orders`,
|
||||||
icon: <IconTruckDelivery size="18" />,
|
icon: <IconTruckDelivery />,
|
||||||
content: <PlaceholderPanel />,
|
content: <PlaceholderPanel />,
|
||||||
hidden: !part.salable
|
hidden: !part.salable
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'test_templates',
|
name: 'test_templates',
|
||||||
label: t`Test Templates`,
|
label: t`Test Templates`,
|
||||||
icon: <IconTestPipe size="18" />,
|
icon: <IconTestPipe />,
|
||||||
content: <PlaceholderPanel />,
|
content: <PlaceholderPanel />,
|
||||||
hidden: !part.trackable
|
hidden: !part.trackable
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'related_parts',
|
name: 'related_parts',
|
||||||
label: t`Related Parts`,
|
label: t`Related Parts`,
|
||||||
icon: <IconLayersLinked size="18" />,
|
icon: <IconLayersLinked />,
|
||||||
content: <RelatedPartTable partId={part.pk ?? -1} />
|
content: <RelatedPartTable partId={part.pk ?? -1} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'attachments',
|
name: 'attachments',
|
||||||
label: t`Attachments`,
|
label: t`Attachments`,
|
||||||
icon: <IconPaperclip size="18" />,
|
icon: <IconPaperclip />,
|
||||||
content: (
|
content: (
|
||||||
<AttachmentTable
|
<AttachmentTable
|
||||||
endpoint={ApiPaths.part_attachment_list}
|
endpoint={ApiPaths.part_attachment_list}
|
||||||
@ -159,33 +170,18 @@ export default function PartDetail() {
|
|||||||
{
|
{
|
||||||
name: 'notes',
|
name: 'notes',
|
||||||
label: t`Notes`,
|
label: t`Notes`,
|
||||||
icon: <IconNotes size="18" />,
|
icon: <IconNotes />,
|
||||||
content: partNotesTab()
|
content: (
|
||||||
|
<NotesEditor
|
||||||
|
url={apiUrl(ApiPaths.part_list, part.pk)}
|
||||||
|
data={part.notes ?? ''}
|
||||||
|
allowEdit={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [part]);
|
}, [part]);
|
||||||
|
|
||||||
function partNotesTab(): React.ReactNode {
|
|
||||||
// TODO: Set edit permission based on user permissions
|
|
||||||
return (
|
|
||||||
<NotesEditor
|
|
||||||
url={apiUrl(ApiPaths.part_list, part.pk)}
|
|
||||||
data={part.notes ?? ''}
|
|
||||||
allowEdit={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function partStockTab(): React.ReactNode {
|
|
||||||
return (
|
|
||||||
<StockItemTable
|
|
||||||
params={{
|
|
||||||
part: part.pk ?? -1
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const breadcrumbs = useMemo(
|
const breadcrumbs = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ name: t`Parts`, url: '/part' },
|
{ name: t`Parts`, url: '/part' },
|
||||||
@ -239,7 +235,7 @@ export default function PartDetail() {
|
|||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<PanelGroup panels={partPanels} />
|
<PanelGroup pageKey="part" panels={partPanels} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -31,7 +31,7 @@ export default function Stock() {
|
|||||||
{
|
{
|
||||||
name: 'stock-items',
|
name: 'stock-items',
|
||||||
label: t`Stock Items`,
|
label: t`Stock Items`,
|
||||||
icon: <IconPackages size="18" />,
|
icon: <IconPackages />,
|
||||||
content: (
|
content: (
|
||||||
<StockItemTable
|
<StockItemTable
|
||||||
params={{
|
params={{
|
||||||
@ -42,8 +42,8 @@ export default function Stock() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'sublocations',
|
name: 'sublocations',
|
||||||
label: t`Sublocations`,
|
label: t`Stock Locations`,
|
||||||
icon: <IconSitemap size="18" />,
|
icon: <IconSitemap />,
|
||||||
content: (
|
content: (
|
||||||
<StockLocationTable
|
<StockLocationTable
|
||||||
params={{
|
params={{
|
||||||
@ -75,7 +75,7 @@ export default function Stock() {
|
|||||||
detail={<Text>{location.name ?? 'Top level'}</Text>}
|
detail={<Text>{location.name ?? 'Top level'}</Text>}
|
||||||
breadcrumbs={breadcrumbs}
|
breadcrumbs={breadcrumbs}
|
||||||
/>
|
/>
|
||||||
<PanelGroup panels={locationPanels} />
|
<PanelGroup pageKey="stocklocation" panels={locationPanels} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -42,37 +42,37 @@ export default function StockDetail() {
|
|||||||
{
|
{
|
||||||
name: 'details',
|
name: 'details',
|
||||||
label: t`Details`,
|
label: t`Details`,
|
||||||
icon: <IconInfoCircle size="18" />,
|
icon: <IconInfoCircle />,
|
||||||
content: <PlaceholderPanel />
|
content: <PlaceholderPanel />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tracking',
|
name: 'tracking',
|
||||||
label: t`Stock Tracking`,
|
label: t`Stock Tracking`,
|
||||||
icon: <IconHistory size="18" />,
|
icon: <IconHistory />,
|
||||||
content: <PlaceholderPanel />
|
content: <PlaceholderPanel />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'allocations',
|
name: 'allocations',
|
||||||
label: t`Allocations`,
|
label: t`Allocations`,
|
||||||
icon: <IconBookmark size="18" />,
|
icon: <IconBookmark />,
|
||||||
content: <PlaceholderPanel />
|
content: <PlaceholderPanel />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'installed_items',
|
name: 'installed_items',
|
||||||
label: t`Installed Items`,
|
label: t`Installed Items`,
|
||||||
icon: <IconBoxPadding size="18" />,
|
icon: <IconBoxPadding />,
|
||||||
content: <PlaceholderPanel />
|
content: <PlaceholderPanel />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'child_items',
|
name: 'child_items',
|
||||||
label: t`Child Items`,
|
label: t`Child Items`,
|
||||||
icon: <IconSitemap size="18" />,
|
icon: <IconSitemap />,
|
||||||
content: <PlaceholderPanel />
|
content: <PlaceholderPanel />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'attachments',
|
name: 'attachments',
|
||||||
label: t`Attachments`,
|
label: t`Attachments`,
|
||||||
icon: <IconPaperclip size="18" />,
|
icon: <IconPaperclip />,
|
||||||
content: (
|
content: (
|
||||||
<AttachmentTable
|
<AttachmentTable
|
||||||
endpoint={ApiPaths.stock_attachment_list}
|
endpoint={ApiPaths.stock_attachment_list}
|
||||||
@ -84,7 +84,7 @@ export default function StockDetail() {
|
|||||||
{
|
{
|
||||||
name: 'notes',
|
name: 'notes',
|
||||||
label: t`Notes`,
|
label: t`Notes`,
|
||||||
icon: <IconNotes size="18" />,
|
icon: <IconNotes />,
|
||||||
content: (
|
content: (
|
||||||
<NotesEditor
|
<NotesEditor
|
||||||
url={apiUrl(ApiPaths.stock_item_list, stockitem.pk)}
|
url={apiUrl(ApiPaths.stock_item_list, stockitem.pk)}
|
||||||
@ -120,7 +120,7 @@ export default function StockDetail() {
|
|||||||
}
|
}
|
||||||
breadcrumbs={breadcrumbs}
|
breadcrumbs={breadcrumbs}
|
||||||
/>
|
/>
|
||||||
<PanelGroup panels={stockPanels} />
|
<PanelGroup pageKey="stockitem" panels={stockPanels} />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,8 @@ export enum ApiPaths {
|
|||||||
category_list = 'api-category-list',
|
category_list = 'api-category-list',
|
||||||
related_part_list = 'api-related-part-list',
|
related_part_list = 'api-related-part-list',
|
||||||
part_attachment_list = 'api-part-attachment-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 URLs
|
||||||
company_list = 'api-company-list',
|
company_list = 'api-company-list',
|
||||||
@ -111,6 +113,10 @@ export function apiEndpoint(path: ApiPaths): string {
|
|||||||
return 'build/attachment/';
|
return 'build/attachment/';
|
||||||
case ApiPaths.part_list:
|
case ApiPaths.part_list:
|
||||||
return 'part/';
|
return 'part/';
|
||||||
|
case ApiPaths.part_parameter_list:
|
||||||
|
return 'part/parameter/';
|
||||||
|
case ApiPaths.part_parameter_template_list:
|
||||||
|
return 'part/parameter/template/';
|
||||||
case ApiPaths.category_list:
|
case ApiPaths.category_list:
|
||||||
return 'part/category/';
|
return 'part/category/';
|
||||||
case ApiPaths.related_part_list:
|
case ApiPaths.related_part_list:
|
||||||
|
Loading…
Reference in New Issue
Block a user